在上一篇推文中,咱們留下的習題是來自《劍指 Offer》 的面試題 26:複雜鏈表的複製。java
請實現複雜鏈表的複製,在複雜鏈表中,每一個結點除了 next 指針指向下一個結點外,還有一個 sibling 指向鏈表中的任意結點或者 NULL。好比下圖就是一個含有 5 個結點的複雜鏈表。node
依舊是咱們熟悉的第一步,先想好咱們的測試用例:面試
測試用例思考完畢,天然是開始思考咱們的測試邏輯了,在思考的過程當中,咱們不妨嘗試和麪試官進行溝通,這樣能夠避免咱們走很多彎路,並且也容易給面試官留下一個善於思考和溝通的好印象。算法
極易想到的邏輯是,咱們先複製咱們傳統的單鏈表,而後再遍歷單鏈表,複製 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;
}
}
}
複製代碼
寫畢代碼,咱們驗證咱們的測試用例。
下一次推文的習題來自於《劍指 Offer》第 29 題:數組中超過一半的數字
面試題:數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字並輸出。好比 {1,2,3,2,2,2,1} 中 2 的次數是 4,數組長度爲 7,因此輸出 2。要求不能修改輸入的數組。
我是南塵,只作比心的公衆號,歡迎關注我。
南塵,GitHub 7k Star,各大技術 Blog 論壇常客,出身 Android,但不只僅是 Android。寫點技術,也吐點情感。作不完的開源,寫不完的矯情,你就聽聽我吹逼,不會錯~