【定义】链表是一种递归的数据结构,它或者为空(null),或者指向一个节点(node)的引用,这个节点含有泛型的元素和一个指向另一条链表的引用。
public class Node {
Item item;
Node next;
}【基本操作】为了维护一个链表,我们需要对链表:创建、插入、删除、遍历等四种操作。
1. 创建(构造)链表:根据链表定义,我们只需要一个Node类型的变量就能表示一条链表,只要保证它的值是null或者指向另一个Node对象且该对象的next域指向了另一条链表即可。比如按一下代码创建链表:
Node first = new Node();
Node second = new Node();
Node third = new Node();
first.item = "to";
second.item = "be";
third.item = "or";
first.next = second;
second.next = third;这时third是一个含单元素的链表,second是一个含双元素(second、third)的链表,first是一个含三个元素(first、second、third)的链表。
2. 在链表中插入元素最容易做到的地方是表头,他所需的时间与表的长度无关。简易代码:
public Node insertFirst() {
Node oldfirst = first;
first = new Node();
first.item = "not";
first.next = oldfirst;
return first;
}3. 接下来你可能需要删除一条链表的首节点,这个操作更简单,只需返回表头节点的next节点即可。简易代码:
public Node deleteFirst() {
if (isEmpty()) throw new NullPointerException();
return first.next;
}4. 如何在表尾插入节点,要完成这一任务,我们需要一个指向链表最后一个节点的链接,因为该节点的链接必须被修改并指向一个含有新元素的新节点。我们不能在链接代码中草率地决定维护一个额外的链接,因为每个修改链表的操作都需要添加检查是否要修改该变量(以及作出相应修改)的代码。比如链表只含有一个元素或者链表为空链表时。简易代码:
public Node getTail() {
Node p = first;
while(p.next != null) p = p.next;
return p;
}5. 如何删除尾节点,last链接帮不上忙,只能遍历整个链表找到指向last链接的节点。简易代码:
public void deleteTail() {
Node p = first, q;
while(p.next != null) {q = p; p = p.next;}
q.next = p.next.next;
}6. 删除指定的节点。
public void Node deleteNode(Node p){
if(isEmpty()) throw new NoSuchElementException();
if(first.next == null) {
if (first == p) first = null;
else throw new NoSuchElementException();
}
Node pPre, q = first;
boolean find = false;
while(q != null) {
if (q == p) { find = true; break;}
pPre = q;
q = q.next;
}
if (!find) throw new NoSuchElementException();
pPre = q.next;
}7. 在指定节点前插入一个新节点。
public void Node insertNode(Node p, Node q){
if(isEmpty()) {first = last = p; }
if (p == first) {
q.next = first;
first = q;
} else {
Node r = findPrior(p);
if (r == null) throw new NoSuchElementException();
q.next = p;
r.next = q;
}
}【常见问题】
1. 反转链表:输入一个链表的头结点,反转该链表,并返回反转后链表的头结点。
首先要画清楚翻转的操作图(下图为一次翻转操作,遍历全链表即可完成整个链表的反转):

然后依图即可写出代码:
public void reverse() {
if (first == null || first.next == null) return;
Node<Item> pPre = null, pNext, pCur = first;
while(pCur != null)
{
pNext = pCur.next;
pCur.next = pPre;
pPre = pCur;
pCur = pNext;
}
first = pPre;
}2. 判断链表是否有环。
这是一个经典的快慢指针问题。通过两个指针,分别从链表的头节点出发,一个每次向后移动一步,另一个移动两步,因为两个指针移动速度不一样,如果存在环,那么两个指针一定会在环里相遇。
一个有环链表如下图示:

相关代码:
public boolean hasCircle()
{
if (first == null || first.next == null) return false;
Node fast, slow;
fast = first;
slow = first;
while(fast != null && fast.next!= null)
{
if (fast.next.next == null) return false;
fast = fast.next.next;
slow = slow.next;
if(fast == slow) return true;
}
return false;
}3. 判断有环链表的入口。
方法一: 暴力求解。先通过快慢指针,找到相遇的节点。遍历此节点得到整个环的元素。然后从链表头再次出发,每走一步与环的元素进行比较。
方法二:假定起点p到环入口点s的距离为a,fast和slow的相交点t与环入口点s的距离为b,环的周长为P,当fast和slow第一次相遇的时候,假定slow走了n 步。那么参考下图有:

slow走的长度:a+b = n;
fast走的长度:a + b + k*P = 2*n;
fast比slow多走了k圈环路,总路程是slow的2倍。
根据上述公式可以得到: n = k*P = a+b,
如果从相遇点t开始,再走 k*P-b (= a)步的话,亦即从s位置走了(k*P -b) + b步,t可以走到s的位置。
算法:设fast回到最初的位置p,每次行进一步,这样fast走了a步的时候,t也走到了s,两者相遇。
相关代码:
public Node findLoopNode()
{
if (first == null || first.next == null) return null;
Node fast, slow;
fast = first;
slow = first;
while(fast != null && fast.next!= null)
{
if (fast.next.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if(fast == slow) break;
}
if(fast != slow) return null;
fast = first;
while(fast != slow)
{
fast = fast.next;
slow = slow.next;
}
return fast;
}4. 求链表相交。
方法一:可转化为环问题求解,将一个链表链接到另一个链表后,通过快慢指针是否相交求解。
方法二:链表相交则其尾部一定一致,因此可以通过判断尾节点是否一致判断。
相关代码略。
5. 两个有序链表合并为一个有序链表。
原理与归并排序merge操作一致,代码略。
道可道,非常道。缘督以为经,可以全生。
















