劍指 offer (2) -- 連結串列篇
上一篇文章中對劍指 offer
中陣列相關的題目進行了歸納,這一篇文章是連結串列篇。同樣地,如果各位大佬發現程式有什麼 bug
或其他更巧妙的思路,歡迎交流學習。
6. 從尾到頭列印連結串列
題目描述
輸入一個連結串列的頭節點,從尾到頭列印連結串列的每個節點的值。
這裡可以用顯式棧,或者遞迴來實現,都比較簡單,也就不多做解釋了。
遞迴實現
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { if(listNode == null){ return new ArrayList<>(); } ArrayList<Integer> list = printListFromTailToHead(listNode.next); list.add(listNode.val); return list; } 複製程式碼
棧實現
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { ArrayList<Integer> list = new ArrayList<Integer>(); if(listNode == null){ return list; } Deque<Integer> stack = new LinkedList<>(); ListNode node = listNode; while(node != null) { stack.push(node.val); node = node.next; } while(!stack.isEmpty()) { list.add(stack.pop()); } return list; } 複製程式碼
18. 刪除連結串列的節點
題目一描述
在 O(1) 時間內刪除連結串列指定節點。給定單鏈表的頭節點引用和一個節點引用,要求在 O(1) 時間內刪除該節點。
解題思路
一般來說,要在單向連結串列中刪除指定節點,需要得到被刪除節點的前驅節點。但這需要從頭節點開始順序查詢,時間複雜度肯定不是 O(1)
了,所以需要換一種思路。
我們可以將後繼節點的值賦值給要刪除的指定節點,再刪除下一個節點,如此也同樣實現了刪除指定節點的功能。但是還需要注意兩種特殊情況:
- 第一種是要刪除的節點是頭節點,這時還需要對連結串列的頭結點進行更新;
- 第二種是要刪除的節點是尾節點,它沒有下一個節點,這時就只能從頭節點開始順序查詢要刪除節點的前驅節點了。
程式碼實現
public Node deleteNode(Node head, Node node) { if (head == null || node == null) { return head; } if (head == node) { // 要刪除的節點是頭節點 return head.next; } else if (node.next == null) { // 要刪除的節點是尾節點 Node cur = head; while (cur.next != node) { cur = cur.next; } cur.next = null; } else { // 要刪除的節點在連結串列中間 ListNode nextNode = node.next; node.val = nextNode.val; node.next = nextNode.next; } return head; } 複製程式碼
這裡除了最後一個節點,其他節點都可以在 O(1)
時間內刪除,只有要刪除的節點是尾節點時,才需要對連結串列進行遍歷,所以,總體的時間複雜度還是 O(1)
。
題目二描述
在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。 例如,連結串列1->2->3->3->4->4->5 處理後為 1->2->5。
解題思路
這裡要刪除排序連結串列中的重複節點,由於頭節點也可能被刪除,所以需要對頭節點特殊處理,或者新增一個虛擬節點。這裡選擇使用虛擬節點。
由於這裡需要判斷當前節點和下一個節點的值,所以迴圈中條件就是要判斷當前節點和下一個節點均不能為空。如果這兩個值不相等,則繼續遍歷。
如果不相等,則迴圈判斷跳過連續重複的數個節點,最後 cur
指向這些重複節點的最後一個。由於重複節點不保留,所以需要讓 pre.next
指向 cur.next
,再更新 cur
為下一個節點 pre.next
,進而繼續判斷。
程式碼實現
public Node deleteDuplication(Node head) { Node dummyHead = new Node(-1); dummyHead.next = head; Node pre = dummyHead; Node cur = head; while (cur != null && cur.next != null) { if (cur.value != cur.next.value) { pre = cur; cur = cur.next; } else { while (cur.next != null && cur.value == cur.next.value) { cur = cur.next; } pre.next = cur.next; cur = pre.next; } } return dummyHead.next; } 複製程式碼
這裡雖然有兩層巢狀迴圈,但實際上只對連結串列遍歷了一遍,所以其時間複雜度為 O(n)
。另外只申請了一個虛擬節點,所以空間複雜度為 O(1)
。
22. 連結串列中倒數第 k 個節點
題目描述
輸入一個連結串列,輸出該連結串列中倒數第 k 個結點。(k 從 1 開始)
解題思路
這裡可以定義兩個指標。第一個指標從連結串列頭開始遍歷,向前移動 k - 1
步。然後從 k
步開始,第二個指標也開始從連結串列頭開始遍歷。
由於兩個指標的距離為 k - 1
,所有當第一個指標移動到連結串列的尾節點時,第二個指標正好移動到倒數第 k
個節點。
程式碼實現
public static ListNode findKthToTail(ListNode head, int k) { if (head == null || k <= 0) { return null; } ListNode fast = head; for (int i = 0; i < k - 1; i++) { if (fast.next == null) { return null; } fast = fast.next; } ListNode slow = head; while (fast.next != null) { fast = fast.next; slow = slow.next; } return slow; } 複製程式碼
23. 連結串列中環的入口節點
題目描述
給一個連結串列,若其中包含環,請找出該連結串列的環的入口結點,否則,輸出null。
解題思路
首先需要判斷連結串列是否有環,可以使用兩個指標,同時從連結串列的頭部開始遍歷,一個指標一次走一步,一個指標一次走兩步。如果快指標能追上慢指標,則表示連結串列有環;否則如果快指標走到了連結串列的末尾,表示沒有環。
在找到環之後,定義一個指標指向連結串列的頭節點,再選擇剛才的慢指標從快慢指標的相遇節點開始,兩個指標同時以每次一步向前移動,它們相遇的節點就是連結串列的入口節點。
程式碼實現
public ListNode EntryNodeOfLoop(ListNode pHead) { if(pHead == null || pHead.next == null) { return null; } ListNode slow = pHead.next; ListNode fast = slow.next; while(slow != fast) { if(fast == null || fast.next == null) { return null; } slow = slow.next; fast = fast.next.next; } ListNode p = pHead; while(slow != p) { slow = slow.next; p = p.next; } return slow; } 複製程式碼
24. 反轉連結串列
題目描述
輸入一個連結串列,反轉連結串列後,輸出新連結串列的表頭。
迴圈解決
思路如下圖:

迴圈程式碼
public ListNode reverseList1(ListNode head) { ListNode newHead = null; ListNode cur = head; ListNode nex; while (cur != null) { nex = cur.next; cur.next = newHead; newHead = cur; // 記錄 cur = nex; } return newHead; } 複製程式碼
遞迴解決

遞迴程式碼
public ListNode reverseList2(ListNode head) { if (head == null || head.next == null) { return head; } ListNode newHead = reverseList2(head.next); head.next.next = head; head.next = null; return newHead; } 複製程式碼
25. 合併兩個有序的連結串列
題目描述
輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,當然我們需要合成後的連結串列滿足單調不減規則。
迴圈解題
在使用迴圈時,首先需要確定新連結串列的頭節點,如果連結串列 first
的頭節點的值小於連結串列 second
的頭節點的值,那麼連結串列 first
的頭節點便是新連結串列的頭節點。
然後迴圈處理兩個連結串列中剩餘的節點,如果連結串列 first
中的節點的值小於連結串列 second
中的節點的值,則將連結串列 first
中的節點新增到新連結串列的尾部,否則新增連結串列 second
中的節點。然後繼續迴圈判斷,直到某一條連結串列為空。
當其中一條連結串列為空後,只需要將另一條連結串列全部連結到新連結串列的尾部。
思路圖如下:

迴圈程式碼
public ListNode merge1(ListNode first, ListNode second) { if (first == null) { return second; } if (second == null) { return first; } ListNode p = first; ListNode q = second; ListNode newHead; if (p.val < q.val) { newHead = p; p = p.next; } else { newHead = q; q = q.next; } ListNode r = newHead; while (p != null && q != null) { if (p.val < q.val) { r.next = p; p = p.next; } else { r.next = q; q = q.next; } r = r.next; } if (p == null) { r.next = q; } else { r.next = p; } return newHead; } 複製程式碼
遞迴解題
使用遞迴解決,比較簡單。首先判斷兩條連結串列是否為空,如果 first
為空,則直接返回 second
;如果 second
為空,則直接返回 first
。
接著判斷連結串列 first
中節點的值和連結串列 second
中節點的值,如果 first
中節點的值較小,則遞迴地求 first.next
和 second
的合併連結串列,讓 first.next
指向新的連結串列頭節點,然後返回 first
即可。
另一種情況類似,這裡就不再贅述了。
遞迴程式碼
public ListNode merge2(ListNode first, ListNode second) { if (first == null) { return second; } if (second == null) { return first; } if (first.val < second.val) { first.next = merge2(first.next, second); return first; } else { second.next = merge2(first, second.next); return second; } } 複製程式碼
35. 複雜連結串列的複製
題目描述
輸入一個複雜連結串列(每個節點中有節點值,以及兩個指標,一個指向下一個節點,另一個特殊指標指向任意一個節點),返回結果為複製後複雜連結串列的head。
解題思路
這可以分為三步來解決。第一步是根據原始連結串列的所有節點,將每一節點的複製節點連結到它的後面。
第二步設定複製出來的節點的特殊指標。如果原始連結串列的節點 p
的特殊指標指向節點 s
,則複製出來的節點 cloned
的特殊指標就指向節點 s
的下一個節點。
第三部是將長連結串列拆分成兩個連結串列,把所有偶數位置的節點連線起來就是新的複製出來的連結串列。
程式碼實現
public RandomListNode Clone(RandomListNode head) { cloneNodes(head); connectSiblingNode(head); return reconnectNodes(head); } private void cloneNodes(RandomListNode head) { RandomListNode p = head; while(p != null) { RandomListNode newNode = new RandomListNode(p.label); newNode.next = p.next; p.next = newNode; p = newNode.next; } } private void connectSiblingNode(RandomListNode head) { RandomListNode p = head; while(p != null) { RandomListNode cloned = p.next; if(p.random != null) { cloned.random = p.random.next; } p = cloned.next; } } private RandomListNode reconnectNodes(RandomListNode head) { RandomListNode p = head; RandomListNode newHead = null; RandomListNode tail = null; if(p != null) { tail = newHead = p.next; p.next = tail.next; p = p.next; } while(p != null) { tail.next = p.next; tail = tail.next; p.next = tail.next; p = p.next; } return newHead; } 複製程式碼
36. 二叉搜尋樹與雙向連結串列
題目描述
輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向。
解題思路
這裡將二叉搜尋樹轉換為一個排序的雙向連結串列,可以採用使用遞迴演算法。
首先遞迴地轉換左子樹,返回其連結串列頭節點,然後需要遍歷該連結串列,找到連結串列的尾節點,這是為了和根節點相連線。需要讓連結串列的尾節點的 right
指向根節點,讓根節點的 left
指向連結串列的尾節點。
然後遞迴地轉換右子樹,返回其連結串列頭節點,然後需要讓根節點的 right
指向連結串列頭節點,讓連結串列的頭節點指向根節點。
最後判斷如果左子樹轉換的連結串列為空,則返回以 root
根節點為頭節點的連結串列,否則返回以左子樹最小值為頭節點的連結串列。
程式碼實現
public TreeNode Convert(TreeNode root) { if(root == null) { return null; } TreeNode leftHead = Convert(root.left); TreeNode leftEnd = leftHead; while(leftEnd != null && leftEnd.right != null) { leftEnd = leftEnd.right; } if(leftEnd != null) { leftEnd.right = root; root.left = leftEnd; } TreeNode rightHead = Convert(root.right); if(rightHead != null) { root.right = rightHead; rightHead.left = root; } return leftHead == null ? root : leftHead; } 複製程式碼
52. 兩個連結串列的第一個公共節點
題目描述
輸入兩個連結串列,找出它們的第一個公共結點。
解題思路
對於兩個連結串列,如果有公共節點,要不它們就是同一條連結串列,要不它們的公共節點一定在公共連結串列的尾部。
可以遍歷兩個連結串列得到它們的長度,然後在較長的連結串列上,先走它們的長度差的步數,接著同時在兩個連結串列上遍歷,如此找到的第一個節點就是它們的第一個公共節點。
程式碼實現
public ListNode findFirstCommonNode(ListNode first, ListNode second) { int length1 = getListLength(first); int length2 = getListLength(second); ListNode headLongList = first; ListNode headShortList = second; int diff = length1 - length2; if (length1 < length2) { headLongList = second; headShortList = first; diff = length2 - length1; } for (int i = 0; i < diff; i++) { headLongList = headLongList.next; } while (headLongList != null && headShortList != null) { if (headLongList == headShortList) { return headLongList; } headLongList = headLongList.next; headShortList = headShortList.next; } return null; } public int getListLength(ListNode head) { int length = 0; ListNode cur = head; while (cur != null) { length++; cur = cur.next; } return length; } 複製程式碼