面試 18:複雜連結串列的複製(劍指 Offer 第 26 題)
在上一篇推文中,我們留下的習題是來自《劍指 Offer》 的面試題 26:複雜連結串列的複製。
請實現複雜連結串列的複製,在複雜連結串列中,每個結點除了 next 指標指向下一個結點外,還有一個 sibling 指向連結串列中的任意結點或者 NULL。比如下圖就是一個含有 5 個結點的複雜連結串列。
提前想好測試用例
依舊是我們熟悉的第一步,先想好我們的測試用例:
- 輸入一個 null ,期望什麼也不輸出;
- 輸入一個結點,sibling 指向自身,期望列印符合題乾的值;
- 輸入多個結點,部分 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;
}
}
}
複製程式碼
驗證測試用例
寫畢程式碼,我們驗證我們的測試用例。
- 輸入一個 null ,也不會輸出,測試通過;
- 輸入一個結點,sibling 指向自身,測試通過;
- 輸入多個結點,部分 sibling 指向 null,測試通過。
課後習題
下一次推文的習題來自於《劍指 Offer》第 29 題:陣列中超過一半的數字
面試題:陣列中有一個數字出現的次數超過陣列長度的一半,請找出這個數字並輸出。比如 {1,2,3,2,2,2,1} 中 2 的次數是 4,陣列長度為 7,所以輸出 2。要求不能修改輸入的陣列。
我是南塵,只做比心的公眾號,歡迎關注我。
南塵,GitHub 7k Star,各大技術 Blog 論壇常客,出身 Android,但不僅僅是 Android。寫點技術,也吐點情感。做不完的開源,寫不完的矯情,你就聽聽我吹逼,不會錯~