劍指Offer-38-兩個連結串列的第一個公共節點

Special__Yang發表於2018-08-11

題目

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

解析

預備知識

2個單向連結串列相交後的示意圖如下所示:
這裡寫圖片描述
從上圖的得知,若兩個連結串列相交,那麼這兩個連結串列應該具有相同的尾部,也就是說呈現出Y型。因為單向連結串列中只有一個next域指向後繼結點,所以從第一個相交點開始都是兩個連結串列的公共部分,而不是我們思維慣性以為相交後就岔開了,不明白的再仔細看看上圖即可。

思路一

空間換時間的做法,既然若相交,必有公共節點。所以我們可以申請一個set,遍歷第一個連結串列,存放所有的節點。然後遍歷第二個節點,若發現set中已存在該節點,那麼就說明從這個節點開始兩個連結串列相交,該節點就是第一個公共節點。

    /**
     * 空間換時間
     * @param pHead1
     * @param pHead2
     * @return
     */
    public static ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if(pHead1 == null || pHead2 == null) {
            return null;
        }
        Set<ListNode> set = new HashSet<>();
        while(pHead1 != null) {
            set.add(pHead1);
            pHead1 = pHead1.next;
        }
        while(pHead2 != null) {
            if(set.contains(pHead2)) {
                return pHead2;
            }
            pHead2 = pHead2.next;
        }
        return null;
    }

思路二

嘗試從不申請額外空間的做法。我們發現若兩個連結串列相交,最後一個節點必然相同。所以我們可以遍歷兩個連結串列使其都走向最後一個節點。此時,若發現最後這兩個節點不相同,則說明連結串列不相交。若相同,則說明連結串列相交。但是最後一個節點可能不是第一個相交點,我們需要向前回溯尋找第一個相同相交點。對於這種先考察後進來的節點,也就是後進先出的情況,使用棧再好不過了,只需在第一步遍歷的時候把連結串列各自的節點放到各自的棧中,最後不斷比較棧頂即可。若棧頂相同,儲存該棧頂,然後彈出,繼續比較,直到棧為空或者棧頂不相同,那麼上一次的棧頂就是第一個公共節點。但是這個做法還是藉助了額外的空間,故程式碼實現就不貼出了。 但是你也可以嘗試遞迴,哈哈,我就不寫了,挺簡單的。
我們要解決就是如何找到第一個相交點,由於連結串列的特性,只能從前往後遍歷。所以問題轉化為如何從開頭遍歷尋找第一個相交點。之前的分析已知知道了,若兩個連結串列相交,必然有公共的尾部。我們的目標是如何使兩個連結串列可以同時走到公共尾部的第一個節點。因為公共尾部的第一個節點到開頭各自不同,所以我們求出2個連結串列的長度的差值,使長的連結串列先走這個差值長度,然後再同時走兩個連結串列,這時就可以不斷比較當前遍歷節點的地址是否相同即可。

    /**
     * 相交的連結串列必有相同的尾部
     * @param pHead1
     * @param pHead2
     * @return
     */
    public static ListNode FindFirstCommonNode2(ListNode pHead1, ListNode pHead2) {
        if(pHead1 == null || pHead2 == null) {
            return null;
        }
        ListNode p = pHead1, q = pHead2;
        int length1 = 1, length2 = 1;
        while(p.next != null) {
            p = p.next;
            length1++;
        }
        while(q.next != null) {
            q = q.next;
            length2++;
        }
        if(p == q) {
            int diff = Math.abs(length1 - length2);
            if(length1 > length2) {
                p = pHead1;
                q = pHead2;
            } else {
                p = pHead2;
                q = pHead1;
            }
            while(diff-- > 0) {
                p = p.next;
            }
            while(p != q) {
                p = p.next;
                q = q.next;
            }
            return p;
        }
        return null;
    }

思路三

牛逼的做法,牛逼的思路,異常的清爽程式碼。
該思路的做法也是如何解決思路中使兩個連結串列遍歷到離尾部相同長度的位置。思路二使用是事先走一遍統計長度,然後使長的連結串列先走差值步長,這樣就可以保證2個連結串列現在離尾部相同距離相同。
該思路則是採用了2個連結串列互補形成2者長度相同的做法。比如我兩個棍子長度為n,m,我可以把m拼在n後面,n拼在m後面。這樣我就有2個相同長度的棍子,n+m。我們實際的做法,並不是真正的拼接,而是跳轉。
1. p,q指向各自的連結串列
2. 判斷p與q是否相同,不相同,判斷p是否走到連結串列1的末尾,若是,則p接著從連結串列2頭部開始走,若沒有,則繼續遍歷下一個節點
3. 判斷q是否走到連結串列2的末尾,若是,則q接著從連結串列1頭部開始走,若沒有,則繼續遍歷下一個節點

以上出現結果可能有以下幾種:
1. 當連結串列1與連結串列2長度一致時,那麼在各自連結串列上遍歷即可完成判斷是否有相交點
2. 因為當長度一致時,兩個連結串列齊頭並進,若有公共尾部,必然能走到相交點,若沒有,最後都會等於null而結束迴圈
3. 當連結串列1與連結串列2長度不一致時,短的連結串列的會率先結束自己的遍歷,並開始在長的連結串列中遍歷。當長的連結串列完成自己的遍歷時,並開始在短的連結串列遍歷時。這時在長的連結串列遍歷已經走完了兩者差值長度。因為是同時走的,這時他們走的距離是相同的,因此他們對於到末尾的距離也是相同,因為2者總長都為n + m。剩下就是繼續判斷是否相交了,因為保證了到末尾的距離一樣,所以必然能同時走到第一個相交點。

    /**
     * 利用補齊法達到使兩個連結串列長度相同
     * @param pHead1
     * @param pHead2
     * @return
     */
    public static ListNode FindFirstCommonNode3(ListNode pHead1, ListNode pHead2) {
        ListNode p = pHead1, q = pHead2;
        while(p != q) {
            p = p == null ? pHead2 : p.next;
            q = q == null ? pHead1 : q.next;
        }
        return p;
    }

總結

可以結合畫圖來發掘思路,但是請注意以上的解法的都是假設連結串列不存在環。若存在環,則可能稍微複雜一點,具體可以參考如何判斷兩個連結串列是否相交併求出相交點如何判斷單連結串列是否有環、環的入口、環的長度和總長

相關文章