在上一篇推文中,咱們留下的習題是來自《劍指 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。要求不能修改輸入的數組。