链表及常见问题

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

posted @ 2017-02-27 21:27  notTao  阅读(1380)  评论(0编辑  收藏  举报