單鏈表複製你已經會了,若是咱們再加個指針...

面試 18:複雜鏈表的複製(劍指 Offer 第 26 題)

在上一篇推文中,咱們留下的習題是來自《劍指 Offer》 的面試題 26:複雜鏈表的複製。java

請實現複雜鏈表的複製,在複雜鏈表中,每一個結點除了 next 指針指向下一個結點外,還有一個 sibling 指向鏈表中的任意結點或者 NULL。好比下圖就是一個含有 5 個結點的複雜鏈表。node

提早想好測試用例

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

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

思考程序邏輯

測試用例思考完畢,天然是開始思考咱們的測試邏輯了,在思考的過程當中,咱們不妨嘗試和麪試官進行溝通,這樣能夠避免咱們走很多彎路,並且也容易給面試官留下一個善於思考和溝通的好印象。算法

極易想到的邏輯是,咱們先複製咱們傳統的單鏈表,而後再遍歷單鏈表,複製 sibling 的指向。數組

假設鏈表中有個結點 A,A 的 sibling 指向結點 B,這個 B 可能在 A 前面也可能在 A 後面,因此咱們惟一的辦法只有從頭結點開始遍歷。對於一個含有 n 個結點的鏈表,因爲定位每一個結點的 sibling 都須要從鏈表頭結點開始通過 O(n) 步才能找到,所以這種方法的時間複雜度是 O(n²)。測試

當咱們告知面試官咱們這樣的思路的時候,面試官告訴咱們,他期待的並非這樣的算法,這樣的算法時間複雜度也過高了,但願能有更加簡單的方式。優化

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

很明顯,咱們上面的想法在定位 sibling 指向上面花了大量的時間,咱們能夠嘗試在這上面進行優化。咱們仍是分爲兩步:第一步仍然是先複製原始鏈表上的每一個結點 N 建立 N1,而後把這些建立出來的結點用 next 鏈接起來。同時咱們把 <N,N1> 的配對信息放在一個哈希表中。第二步是設置複製鏈表的 sibling 指向,若是原始鏈表中有 N 指向 S,那麼咱們的複製鏈表中必然存在 N1 指向 S1 。因爲有了哈希表,咱們能夠用 O(1) 的時間,根據 S 找到 S1。spa

這樣的方法下降了時間成本,咱們高興地與面試官分享咱們的想法,卻被面試官指出,這樣的想法雖然把時間複雜度下降到了 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。寫點技術,也吐點情感。作不完的開源,寫不完的矯情,你就聽聽我吹逼,不會錯~

相關文章
相關標籤/搜索