單連結串列複製你已經會了,如果我們再加個指標...

nanchen2251發表於2019-03-04

面試 18:複雜連結串列的複製(劍指 Offer 第 26 題)

在上一篇推文中,我們留下的習題是來自《劍指 Offer》 的面試題 26:複雜連結串列的複製。

請實現複雜連結串列的複製,在複雜連結串列中,每個結點除了 next 指標指向下一個結點外,還有一個 sibling 指向連結串列中的任意結點或者 NULL。比如下圖就是一個含有 5 個結點的複雜連結串列。

單連結串列複製你已經會了,如果我們再加個指標...

提前想好測試用例

依舊是我們熟悉的第一步,先想好我們的測試用例:

  1. 輸入一個 null ,期望什麼也不輸出;
  2. 輸入一個結點,sibling 指向自身,期望列印符合題乾的值;
  3. 輸入多個結點,部分 sibling 指向 null,期望列印符合題乾的值。

思考程式邏輯

測試用例思考完畢,自然是開始思考我們的測試邏輯了,在思考的過程中,我們不妨嘗試和麵試官進行溝通,這樣可以避免我們走不少彎路,而且也容易給面試官留下一個善於思考和溝通的好印象。

極易想到的邏輯是,我們先複製我們傳統的單連結串列,然後再遍歷單連結串列,複製 sibling 的指向。

假設連結串列中有個結點 A,A 的 sibling 指向結點 B,這個 B 可能在 A 前面也可能在 A 後面,所以我們唯一的辦法只有從頭結點開始遍歷。對於一個含有 n 個結點的連結串列,由於定位每個結點的 sibling 都需要從連結串列頭結點開始經過 O(n) 步才能找到,因此這種方法的時間複雜度是 O(n²)。

當我們告知面試官我們這樣的思路的時候,面試官告訴我們,他期待的並不是這樣的演算法,這樣的演算法時間複雜度也太高了,希望能有更加簡單的方式。

得到了面試官的訴求,我們再來看看我們前面的想法時間都花在哪兒去了。

很明顯,我們上面的想法在定位 sibling 指向上面花了大量的時間,我們可以嘗試在這上面進行優化。我們還是分為兩步:第一步仍然是先複製原始連結串列上的每個結點 N 建立 N1,然後把這些建立出來的結點用 next 連線起來。同時我們把 <N,N1> 的配對資訊放在一個雜湊表中。第二步是設定複製連結串列的 sibling 指向,如果原始連結串列中有 N 指向 S,那麼我們的複製連結串列中必然存在 N1 指向 S1 。由於有了雜湊表,我們可以用 O(1) 的時間,根據 S 找到 S1。

這樣的方法降低了時間成本,我們高興地與面試官分享我們的想法,卻被面試官指出,這樣的想法雖然把時間複雜度降低到了 O(n),但卻由於雜湊表的存在,需要 O(n) 的空間,而他所期望的方法是不佔用任何輔助空間的。

接下來我們再換一下思路,不用輔助空間,我們卻要用更少的實際解決 sibling 的指向問題。

我們前面似乎對於指向都採用過兩個指標的方法,這裡似乎可以用類似的處理方式處理。

我們不妨利用原有連結串列對每個結點 N 在後面直接在後面建立 N1,這樣相當於我們擴長原始連結串列長度為現有連結串列的 2 倍,奇數位置的結點連線起來是原始連結串列,偶數位置的結點連線起來就是我們的複製連結串列。

開始編寫程式碼

我們先完成第一部分的程式碼。根據原始連結串列的每個結點 N ,建立 N1,並把 N 的 next 指向 N1,N1 的 next 指向 N 的 next。

private static void cloneNodes(Node head) {
    Node node = null;
    while (head != null) {
        // 先新建結點
        node = new Node(head.data);
        // 再把head 的 next 指向 node 的 next
        node.next = head.next;
        // 然後把 node 作為 head 的 next
        head.next = node;
        // 最後遍歷條件
        head = node.next;
    }
}
複製程式碼

上面完成了複製結點,下面我們需要編寫 sibling 的指向複製。

我們的思想是:當 N 執行 S,那麼 N1 就應該指向 S1,即 N.next.sibling = N.sibling.next;

private static void connectNodes(Node head) {
    while (head != null) {
        if (head.sibling != null) {
            //如果 當前結點的 sibling 不為 null,那就把它後面的複製結點指向當前sibling指向的下一個結點
            head.next.sibling = head.sibling.next;
        }
        // 遍歷
        head = head.next.next;
    }
}
複製程式碼

最後我們只需要拿出原本的連結串列(奇數)和複製的連結串列(偶數)即可。

private static Node reconnectList(Node head) {
    if (head == null)
        return null;
    // 用於存放複製連結串列的頭結點
    Node cloneHead = head.next;
    // 用於記錄當前處理的結點
    Node temp = cloneHead;
    // head 的 next 還是要指向原本的 head.next
    // 實際上現在由於複製後,應該是 head.next.next,即cloneHead.next
    head.next = cloneHead.next;
    // 指向新的被複制結點
    head = head.next;
    while (head != null) {
        // temp 代表的是複製結點
        // 先進行賦值
        temp.next = head.next;
        // 賦值結束應該給 next 指向的結點賦值
        temp = temp.next;
        // head 的下一個結點應該指向被賦值的下一個結點
        head.next = temp.next;
        head = temp.next;
    }
    return cloneHead;
}
複製程式碼

合併後的最終程式碼就是:

public class Test18 {

    private static class Node {
        int data;
        Node next;
        Node sibling;

        Node(int data) {
            this.data = data;
        }
    }

    private static Node complexListNode(Node head) {
        if (head == null)
            return null;
        // 第一步,複製結點,並用 next 連線
        cloneNodes(head);
        // 第二步,把 sibling 也複製起來
        connectNodes(head);
        // 第三步,返回偶數結點,連線起來就是複製的連結串列
        return reconnectList(head);
    }

    private static void cloneNodes(Node head) {
        Node node = null;
        while (head != null) {
            // 先新建結點
            node = new Node(head.data);
            // 再把head 的 next 指向 node 的 next
            node.next = head.next;
            // 然後把 node 作為 head 的 next
            head.next = node;
            // 最後遍歷條件
            head = node.next;
        }
    }

    private static void connectNodes(Node head) {
        while (head != null) {
            if (head.sibling != null) {
                // 如果 當前結點的 sibling 不為 null,那就把它後面的複製結點指向當前sibling指向的下一個結點
                head.next.sibling = head.sibling.next;
            }
            // 遍歷
            head = head.next.next;
        }
    }

    private static Node reconnectList(Node head) {
        if (head == null)
            return null;
        // 用於存放複製連結串列的頭結點
        Node cloneHead = head.next;
        // 用於記錄當前處理的結點
        Node cloneNode = cloneHead;
        // head 的 next 還是要指向原本的 head.next
        // 實際上現在由於複製後,應該是 head.next.next,即cloneHead.next
        head.next = cloneHead.next;
        // 因為我們第一個結點已經拆分了,所以需要指向新的被複制結點才可以開始迴圈
        head = head.next;
        while (head != null) {
            // cloneNode 代表的是複製結點
            // 先進行賦值
            cloneNode.next = head.next;
            // 賦值結束應該給 next 指向的結點賦值
            cloneNode = cloneNode.next;
            // head 的下一個結點應該指向被賦值的下一個結點
            head.next = cloneNode.next;
            head = cloneNode.next;
        }
        return cloneHead;
    }


    public static void main(String[] args) {
        Node head1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        Node node4 = new Node(4);
        Node node5 = new Node(5);
        head1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;
        node5.next = null;
        head1.sibling = node4;
        node2.sibling = null;
        node3.sibling = node5;
        node4.sibling = node2;
        node5.sibling = head1;

        print(head1);
        Node root = complexListNode(head1);
        System.out.println();
        print(head1);
        print(root);
        System.out.println();
        System.out.println(isSameLink(head1, root));
    }

    private static boolean isSameLink(Node head, Node root) {
        while (head != null && root != null) {
            if (head == root) {
                head = head.next;
                root = root.next;
            } else {
                return false;
            }
        }
        return head == null && root == null;
    }

    private static void print(Node head) {
        Node temp = head;
        while (head != null) {
            System.out.print(head.data + "->");
            head = head.next;
        }
        System.out.println("null");
        while (temp != null) {
            System.out.println(temp.data + "=>" + (temp.sibling == null ? "null" : temp.sibling.data));
            temp = temp.next;
        }
    }
}
複製程式碼

驗證測試用例

寫畢程式碼,我們驗證我們的測試用例。

  1. 輸入一個 null ,也不會輸出,測試通過;
  2. 輸入一個結點,sibling 指向自身,測試通過;
  3. 輸入多個結點,部分 sibling 指向 null,測試通過。

課後習題

下一次推文的習題來自於《劍指 Offer》第 29 題:陣列中超過一半的數字

面試題:陣列中有一個數字出現的次數超過陣列長度的一半,請找出這個數字並輸出。比如 {1,2,3,2,2,2,1} 中 2 的次數是 4,陣列長度為 7,所以輸出 2。要求不能修改輸入的陣列。


我是南塵,只做比心的公眾號,歡迎關注我。

南塵,GitHub 7k Star,各大技術 Blog 論壇常客,出身 Android,但不僅僅是 Android。寫點技術,也吐點情感。做不完的開源,寫不完的矯情,你就聽聽我吹逼,不會錯~

單連結串列複製你已經會了,如果我們再加個指標...

相關文章