【定义】链表是一种递归的数据结构,它或者为空(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操作一致,代码略。