1. 程式人生 > >劍指Offer程式設計題筆記之連結串列相關

劍指Offer程式設計題筆記之連結串列相關

前言

本來呢,是想十題十題這樣寫幾篇筆記的。後面發現,按照題型來分類會更好。比如這篇,雖然只有九題,但是都是跟連結串列相關的。按題型分類,這樣,也好做最後的總結,是吧?

題目

從尾到頭列印連結串列

第1題

題目描述
輸入一個連結串列,從尾到頭列印連結串列每個節點的值。

思路:
立馬想到的是有先進後出特性的棧!先把節點值存到棧中,再通過出棧方法輸出節點值。

實現如下:

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    Stack<Integer> stack = new
Stack<>(); ListNode p = listNode; while(p!=null){ stack.push(p.val); p = p.next; } ArrayList<Integer> list = new ArrayList<>(); while(!stack.isEmpty()){ list.add(stack.pop()); } return list; }

連結串列中倒數第K個節點

第2題

題目描述
輸入一個連結串列,輸出該連結串列中倒數第k個結點。

思路:
看到倒數,也就是反序,就想到了棧。但是棧會耗費額外的空間記憶體。實現略。

思路2:
遍歷一遍,得到連結串列個數。第二次遍歷,就知道倒數第K個時哪個了。但需要遍歷兩遍。實現略。

思路3:
使用兩個指標ab,a指標先走K步,然後ab兩個指標同時前進,當a指標到達尾部時,b指標所指向的節點正是倒數第K個節點。因為a指標先走了k步,所以ab節點差了k個位置啊。這樣只需要遍歷一次,且沒有額外空間開銷。

public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        if
(head==null) return null; ListNode p1 = head; ListNode p2 = head; int i=1;//i用來記錄實際先走了多少步 while(i<k && p1.next!=null){ p1 = p1.next; i++; } if(i!=k)//如果i不等於k,說明i小於k,說明k比連結串列長度還長 return null; while(p1.next!=null){ p1 = p1.next; p2 = p2.next; } return p2; } }

大神更簡潔的實現:

public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        ListNode p,q;
        p = head;
        q = head;
        int i=0;
        for(;p!=null;i++){
            if(i>=k)
                q = q.next;
            p = p.next;
        }
        return i<k?null:q;
    }
}

相比之下,我的寫法程式碼重複率大很多。

反轉連結串列

第3題

題目描述
輸入一個連結串列,反轉連結串列後,輸出連結串列的所有元素。

思路:第一個想到的又是棧…要使用額外記憶體空間啊。實現略。

思路2:不使用額外記憶體空間!利用三個指標,遍歷的同時實現節點next域的反轉。

實現如下:

public class Solution {
    public ListNode ReverseList(ListNode head) {
        if(head==null||head.next==null)
            return head;
        ListNode p1 = head;
        ListNode p2 = p1.next;
        ListNode p3 = p2.next;
        do{
            p2.next = p1;   //實現反轉,下面重新設定指標
            p1 = p2;
            p2 = p3;
            if(p3!=null && p3.next!=null)
                p3 = p3.next;
            else
                p3 = null;
        }while(p2!=null);
        head.next = null;
        return p1;
    }
}

利用三個指標,另一種寫法:

public class Solution {
    public ListNode ReverseList(ListNode head) {
        if(head==null)
            return null;
        ListNode p1 = head;
        ListNode p2 = p1.next;
        ListNode p3;
        p1.next = null;
        while(p1!=null && p2!=null){
            p3 = p2.next;
            p2.next = p1;
            p1 = p2;
            p2 = p3;
        }
        return p1;
    }
}

合併兩個排序的連結串列

第4題

題目描述
輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,當然我們需要合成後的連結串列滿足單調不減規則。

思路:
利用一個指標(作為新連結串列的尾指標),另外關鍵是建立一個頭節點,通過頭節點和一個指標,將兩個連結串列串起來。
具體是當比較兩條連結串列的節點時,找到那個值小的節點,並讓它作為尾指標指向的節點的下一個節點,再重新更新尾指標。

實現如下:

public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        if(list1==null)
            return list2;
        if(list2==null)
            return list1;
        ListNode preNode = new ListNode(0);//頭節點
        ListNode p = preNode;
        while(list1!=null && list2!=null){
            if(list1.val<=list2.val){
                p.next = list1;
                p = list1;
                list1 = list1.next;
            }else{
                p.next = list2;
                p = list2;
                list2 = list2.next;
            }
            if(list1==null)     //這兩步不能漏
                p.next = list2;
            if(list2==null)     //這兩步不能漏
                p.next = list1;
        }
        return preNode.next;
    }
}

複雜連結串列的複製

第5題

題目描述
輸入一個複雜連結串列(每個節點中有節點值,以及兩個指標,一個指向下一個節點,另一個特殊指標指向任意一個節點),返回結果為複製後複雜連結串列的head。(注意,輸出結果中請不要返回引數中的節點引用,否則判題程式會直接返回空)

思路:
思路???就建立個頭結點,然後就遍歷舊連結串列,複製節點,串成新連結串列啊。不然還能怎樣。這題是想考我什麼啊。

實現如下:

public class Solution {
    public RandomListNode Clone(RandomListNode pHead)
    {
        if(pHead==null)
            return null;
        RandomListNode preHead = new RandomListNode(0);
        RandomListNode p = preHead;
        while(pHead!=null){
            RandomListNode node = new RandomListNode(pHead.label);
            node.next = pHead.next;
            node.random = pHead.random;
            p.next = node;
            p = node;
            pHead = pHead.next;
        }
        return preHead.next;
    }
}

遞迴實現:

public class Solution {
    public RandomListNode Clone(RandomListNode pHead)
    {
        if(pHead==null)
            return null;
        RandomListNode node = new RandomListNode(pHead.label);
        node.random = pHead.random;
        node.next = Clone(pHead.next);
        return node;    //不能漏
    }
}

兩個連結串列的第一個公共節點

第6題

題目描述
輸入兩個連結串列,找出它們的第一個公共結點。

一開始想到的思路是暴力法,比較每個節點!後來才明白,只有有一個節點是同一個節點,往後的節點都是不需要比較的,因為往後都是同樣的節點啊,肯定相同啊。

暴力思路(可不看):
兩條連結串列ab,
a:1-2-3-4-5
b:2-3-4-5
連結串列a,可以看成是5個連結串列,分別是:
a1: 1-2-3-4-5
a2: 2-3-4-5
a3: 3-4-5
a4: 4-5
a5: 5
判斷連結串列a1是否連結串列b的後半部分,是的話則返回公共節點a1,否則
判斷連結串列a2是否連結串列b的後半部分,是的話則返回公共節點a2,否則

最後a5都不滿足,則返回null。

實現如下(可不看):

public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if(pHead1==null || pHead2==null)
            return null;
        while(pHead2!=null){        //每層迴圈,p2指向的就是上面思路中的a1,a2,a3,a4,a5
            ListNode p1 = pHead1;   //連結串列a的指標
            ListNode p2 = pHead2;   //連結串列b的指標
            while(p1!=null&&p2!=null){  //判斷p2指向的連結串列是否為連結串列pHead的後半部分
                if(p1.val==p2.val){
                    p1 = p1.next;
                    p2 = p2.next;
                }else{
                    p1 = p1.next;
                }
            }
            if(p1==null&&p2==null)  //遍歷到最後為null,說明到尾節點都是相同,則說明從pHead2開始兩連結串列就是相同的
                return pHead2;
            pHead2 = pHead2.next;   //以下個節點作為頭結點
        }
        return null;
    }
}

另一種思路:
先遍歷兩條連結串列,獲取兩條連結串列長度,計算差值x。然後讓較長的連結串列先走x步。然後,兩條連結串列就一樣長了。遍歷兩連結串列,返回第一個相等的節點。這種解法空間複雜度為O(1)。

實現如下:

public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if(pHead1==null || pHead2==null)
            return null;
        int p1Len = getLength(pHead1);
        int p2Len = getLength(pHead2);
        int x = 0;
        if(p1Len>=p2Len){
            x = p1Len-p2Len;
            pHead1 = getNextN(pHead1,x);
        }else{
            x = p2Len-p1Len;
            pHead2 = getNextN(pHead2,x);
        }
        while(pHead1!=pHead2){
            pHead1 = pHead1.next;
            pHead2 = pHead2.next;
        }
        return pHead1;

    }
    private int getLength(ListNode p){
        int n=0;
        while(p!=null){
            n++;
            p=p.next;
        }
        return n;
    }
    private ListNode getNextN(ListNode p,int x){
        int i=0;
        while(i<x){
            i++;
            p = p.next;
        }
        return p;
    }
}

另一個思路:
想起了大神解法,利用兩個棧!!把兩條連結串列壓入兩個棧,再依次比較兩棧棧頂的節點,若相同,則彈出,繼續比較棧頂節點。如此迴圈,就可以實現節點從後往前一個一個對比了!雖然需要額外維護兩個棧,但是該解法卻很巧妙。

實現如下:

public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if(pHead1==null || pHead2==null)
            return null;
        Stack<ListNode> stack1 = new Stack();
        Stack<ListNode> stack2 = new Stack();
        while(pHead1 != null){
            stack1.push(pHead1);
            pHead1 = pHead1.next;
        }
        while(pHead2 != null){
            stack2.push(pHead2);
            pHead2 = pHead2.next;
        }
        ListNode result = null;
        while(!stack1.isEmpty() && !stack2.isEmpty() && stack1.peek()==stack2.peek()){
            result = stack1.pop();
            stack2.pop();
        }
        return result;
    }
}

雖然該思路不是我先想到的,但是沒有學完就忘,還是可以的吧?

連結串列中環的入口節點

第7題

題目描述
一個連結串列中包含環,請找出該連結串列的環的入口結點。

思路:
如果可以使用額外記憶體空間的話,那麼可以建立一個ArrayList,用來儲存節點,然後每次呼叫add方法往容器新增節點前,先用contains方法判斷容器裡是否該節點,若存在,則說明該節點就是重複出現的節點,即環的入口節點。若果不存在,則新增進容器。

實現如下:

public class Solution {
    public ListNode EntryNodeOfLoop(ListNode pHead){
        ArrayList<ListNode> list = new ArrayList();
        ListNode p = pHead;
        while(p!=null){
            if(list.contains(p))
                return p;
            else
                list.add(p);
            p = p.next;
        }
        return null;
    }
}

另一種解法:
空間複雜度為O(1)的高階解法,使用快慢指標。還沒搞懂。弄懂了再補上。
參考連結

刪除連結串列中的重複的節點

第8題

題目描述
在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。 例如,連結串列1->2->3->3->4->4->5 處理後為 1->2->5

思路:
注意,題目說了,這是排序的連結串列。如果沒排序,就另當別論了。
建立一個頭結點和尾指標。關鍵是用到尾指標。遍歷連結串列,當找到節點值相等的兩個節點,設其節點值為x,然後通過迴圈將節點值為x的移除。怎麼移除?藉助尾指標,通過操控尾指標來剔除那些重複的節點。
要注意的是,這個尾指標並不是相對於舊連結串列而言,而是相對於無重複節點的連結串列而已。

實現如下:

public class Solution {
    public ListNode deleteDuplication(ListNode pHead){
        ListNode preNode = new ListNode(0);
        preNode.next = pHead;
        ListNode last = preNode;
        ListNode p = pHead;
        while(p!=null&&p.next!=null){//遍歷連結串列
            if(p.val==p.next.val){
                int val = p.val;
                while(p!=null&&p.val==val){//剔除重複節點
                    p = p.next;
                    last.next = p;  //剔除
                }
            }else{  //如果不重複,則無需剔除,直接移動尾指標
                last = p;
                p = p.next;
            }
        }
        return preNode.next;
    }
}

二叉搜尋樹變雙向連結串列

第9題

題目描述
輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向。

思路:
通過中序遍歷實現,關鍵是要建立一個成員引用pre。用來表示上一個剛被訪問過的節點,也可看做是新連結串列的尾指標。
通過在遍歷中設定pre.right=current,current.left=pre,pre=current,即可將二叉樹轉變成雙向連結串列。

實現如下:

public class Solution {
    TreeNode pre = null;
    public TreeNode Convert(TreeNode pRootOfTree) {
        TreeNode p = pRootOfTree;
        if(p==null)
            return p;
        middleTraverse(p);
        //通過向左遍歷找到連結串列的首結點
        while(p.left!=null)
            p = p.left;
        return p;

    }
    //中序遍歷
    public void middleTraverse(TreeNode cur){
        if(cur==null)
            return;

        middleTraverse(cur.left);

        //關鍵,重新調整節點的指標
        if(pre!=null)
            pre.right = cur;
        cur.left = pre;
        pre = cur;

        middleTraverse(cur.right);
    }
}

總結

連結串列相關的題,很多優秀的解法空間複雜度都為O(1),通常呢是利用一個或多個指標,或者使用快慢指標來遍歷(像題2,題3,題8)。有可能需要多次遍歷(一次用來獲取連結串列長度,像題6的第二種解法)。有時,還需要用一個尾指標來維護新的連結串列,像題4、題8、題9。如果出現反序遍歷(像題1)或者從尾部開始比較(像題6),則可以考慮使用棧。再不然,可以使用ArrayList、HashMap來配合解決問題(像題8)。