鏈結串列 — 實戰題解
範圍 — linked_list.md 背後的題目集:每題歸檔在它歸約到的核心模板之下,寫成一張題目卡,點出它多加的那一步;模板本身與各模板的錨點題都放在核心文件。 另見:linked_list.md — 這些題目歸約到的七個模板;2_pointers_linkedlist.md — 環偵測、相交與回文判斷,在那裡詳寫;heap.md — LC 23 的 k 路合併;hash_map.md — LC 1171 背後的前綴和表,以及 LC 138 背後的節點對照表;monotonic_stack.md — LC 1019 背後的堆疊。
LeetCode 題目清單
總覽
linked_list.md 收著模板 — 迭代反轉、虛擬頭節點、兩個虛擬節點、反轉一段、快慢指標、進位迴圈,以及雜湊表加雙向鏈結串列 — 每個都在它的錨點題上詳寫。這份文件是套用它們的題目,每個模板一節,順序相同。每張卡說明它歸約到哪個模板、多加了哪一步;在別的文件詳寫過的題目只放一列指標,不放第二份。
索引
| LC | 題目 | 歸約到 | 難度 | 位置 |
|---|---|---|---|---|
| 143 | Reorder List | linked_list § Template 1 | Medium | 題目卡 |
| 1019 | Next Greater Node In Linked List | monotonic_stack § Template 1 | Medium | 題目卡 |
| 19 | Remove Nth Node From End of List | linked_list § Template 2 | Medium | 題目卡 |
| 82 | Remove Duplicates from Sorted List II | linked_list § Template 2 | Medium | 詳寫於 2_pointers_linkedlist § 2-2 |
| 147 | Insertion Sort List | linked_list § Template 2 | Medium | 題目卡 |
| 1171 | Remove Zero Sum Consecutive Nodes from Linked List | linked_list § Template 2 + hash_map § Template 4 | Medium | 題目卡 |
| 21 | Merge Two Sorted Lists | linked_list § Template 3 | Easy | 題目卡 |
| 725 | Split Linked List in Parts | linked_list § Template 3 | Medium | 題目卡 |
| 817 | Linked List Components | linked_list § Template 3 | Medium | 題目卡 |
| 23 | Merge k Sorted Lists | heap § Template 3 | Hard | 詳寫於 heap.md |
| 92 | Reverse Linked List II | linked_list § Template 4 | Medium | 錨點題,詳寫於 linked_list.md |
| 24 | Swap Nodes in Pairs | linked_list § Template 4 | Medium | 題目卡 |
| 430 | Flatten a Multilevel Doubly Linked List | linked_list § Template 4 | Medium | 題目卡 |
| 25 | Reverse Nodes in k-Group | linked_list § Template 4 | Hard | 題目卡 |
| 160 | Intersection of Two Linked Lists | 2_pointers_linkedlist § 1-1-5 | Easy | 詳寫於 2_pointers_linkedlist.md |
| 234 | Palindrome Linked List | linked_list § Template 5 | Easy | 詳寫於 2_pointers_linkedlist § 1-1-6 |
| 142 | Linked List Cycle II | 2_pointers_linkedlist § 1-1-2 | Medium | 詳寫於 2_pointers_linkedlist.md |
| 148 | Sort List | linked_list § Template 5 | Medium | 詳寫於 advanced_divide_and_conquer § LC 148 |
| 369 | Plus One Linked List | linked_list § Template 6 | Medium | 題目卡 |
| 146 | LRU Cache | linked_list § Template 7 | Medium | 錨點題,詳寫於 linked_list.md |
| 138 | Copy List with Random Pointer | hash_map § Template 8 | Medium | 題目卡 |
| 460 | LFU Cache | linked_list § Template 7,LFU 變體 | Hard | 詳寫於 linked_list.md |
迭代反轉題目
Reorder List — LC 143
關鍵 — 答案是前半段與反轉後的後半段交錯。在第一個中點之後切開, 兩半就不共用任何節點;反轉後半段,再輪流各取一個節點。後半段永遠不會比較長, 所以迴圈跑到後半段用完為止。
歸約到:linked_list § Template 1 — 只對後半段跑,後半段用 Template 5 的第一個中點迴圈找出,最後再一趟交錯走訪。
# python
# LC 143 - Reorder List
# IDEA: first middle (fast/slow) -> cut -> reverse the back half -> interleave the two halves
# time = O(n), space = O(1)
class Solution(object):
def reorderList(self, head):
if not head or not head.next:
return
# 1) first middle: the left half is never the shorter one
slow = fast = head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
# 2) cut after the middle, then reverse the back half (Template 1)
second = slow.next
slow.next = None
prev = None
while second:
nxt = second.next
second.next = prev
prev = second
second = nxt
# 3) interleave: one node from the front half, one from the reversed back half
first, second = head, prev
while second:
first_next = first.next # cache both successors before relinking
second_next = second.next
first.next = second
second.next = first_next
first = first_next
second = second_next
// java
// LC 143 - Reorder List
// IDEA: first middle (fast/slow) -> cut -> reverse the back half -> interleave the two halves
// time = O(n), space = O(1)
public void reorderList(ListNode head) {
if (head == null || head.next == null) {
return;
}
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode second = slow.next;
slow.next = null; // cut: the halves must not share a node
ListNode prev = null;
while (second != null) {
ListNode next = second.next;
second.next = prev;
prev = second;
second = next;
}
ListNode first = head;
second = prev;
while (second != null) {
ListNode firstNext = first.next;
ListNode secondNext = second.next;
first.next = second;
second.next = firstNext;
first = firstNext;
second = secondNext;
}
}
試跑([1,2,3,4,5])
first middle: slow = 3 front = 1 -> 2 -> 3 back = 4 -> 5
reverse back: back = 5 -> 4
interleave: 1 -> 5 -> 2 -> 4 -> 3
陷阱
- 漏掉切斷
slow.next = None:在[1,2,3,4]上,節點 2 仍指著 3,反轉後的後半段也以 3 結尾,交錯之後形成環3 -> 3。 - 在
first.next = second之後才讀first.next:一步之後前半段就弄丟了。
相似題 — LC 234 (中點 + 反轉,再比較而不是交錯) · LC 2130 (中點 + 反轉,再兩兩求和)
Next Greater Node In Linked List — LC 1019
關鍵 — 堆疊裡放的是還在等更大值的索引,值由底到頂遞減。新值會彈出每個比它小的索引, 並成為它們的答案;最後留在堆疊裡的沒有答案,維持
0。串列只會被讀, 所以先複製成陣列:堆疊需要索引。
歸約到:monotonic_stack § Template 1 — 唯一的改變是用一趟 Template 1 式的走訪,把串列轉成陣列。
# python
# LC 1019 - Next Greater Node In Linked List
# IDEA: list -> array, then a decreasing stack of indices; a larger value answers every index it pops
# time = O(n), space = O(n)
class Solution(object):
def nextLargerNodes(self, head):
vals = []
while head:
vals.append(head.val)
head = head.next
res = [0] * len(vals) # 0 = no greater node
stack = [] # indices, values decreasing bottom -> top
for i, v in enumerate(vals):
while stack and vals[stack[-1]] < v:
res[stack.pop()] = v
stack.append(i)
return res
// java
// LC 1019 - Next Greater Node In Linked List
// IDEA: list -> array, then a decreasing stack of indices; a larger value answers every index it pops
// time = O(n), space = O(n)
public int[] nextLargerNodes(ListNode head) {
List<Integer> vals = new ArrayList<>();
for (ListNode cur = head; cur != null; cur = cur.next) {
vals.add(cur.val);
}
int n = vals.size();
int[] res = new int[n]; // 0 = no greater node
Deque<Integer> stack = new ArrayDeque<>(); // indices, values decreasing
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && vals.get(stack.peek()) < vals.get(i)) {
res[stack.pop()] = vals.get(i);
}
stack.push(i);
}
return res;
}
試跑([2,7,4,3,5])
i=0 v=2 stack=[0]
i=1 v=7 pop 0 (res[0]=7) stack=[1]
i=2 v=4 stack=[1,2]
i=3 v=3 stack=[1,2,3]
i=4 v=5 pop 3 (res[3]=5), pop 2 (res[2]=5) stack=[1,4]
res = [7,0,5,5,0]
陷阱
- 用
<=而不是<來彈出:相等的值不算更大,所以[1,1,2]會得到[1,2,0],而不是[2,2,0]。 - 推入值而不是索引:你就沒辦法知道一次彈出是誰的答案。
虛擬頭節點刪除題目
Remove Nth Node From End of List — LC 19
關鍵 —
fast和slow都從虛擬節點出發,先讓fast走 n + 1 步;當fast走出尾端時,slow正站在目標的前一個節點。從虛擬節點出發, 才能讓目標就是 head 時也成立。
歸約到:linked_list § Template 2 — 虛擬節點,再加上兩個指標之間的固定間隔,取代值的比對。
# python
# LC 19 - Remove Nth Node From End of List
# IDEA: start fast and slow on the dummy, open a gap of n + 1, walk both until fast falls off
# time = O(n), space = O(1)
class Solution(object):
def removeNthFromEnd(self, head, n):
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
for _ in range(n + 1): # the gap: slow will stop one node before the target
fast = fast.next
while fast: # walk both until fast falls off the end
fast = fast.next
slow = slow.next
slow.next = slow.next.next # unlink the target
return dummy.next # the head may have been the target
// java
// LC 19 - Remove Nth Node From End of List
// IDEA: start fast and slow on the dummy, open a gap of n + 1, walk both until fast falls off
// time = O(n), space = O(1)
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy, slow = dummy;
for (int i = 0; i < n + 1; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
試跑 — 刪掉 head([1,2,3,4,5]、n = 5),也就是虛擬節點存在的理由:
fast = slow = dummy
fast moves n+1 = 6 steps: dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None (fast = None)
while fast: never runs slow stays on dummy, one node before the head
slow.next = slow.next.next dummy -> 2 -> 3 -> 4 -> 5
return dummy.next [2,3,4,5]
Without the dummy, slow starts ON the head: it can never stand before it, and
slow.next = slow.next.next removes 2 instead of 1 unless a special case returns head.next.
陷阱
- 兩個都從
head出發、間隔 n:[1,2,3,4,5]、n = 2 時正確,但n = 5需要一個if fast is None: return head.next分支,正是虛擬節點省掉的那個。 - 回傳
head而不是dummy.next:[1]、n = 1,會回傳已刪掉的節點,而不是[]。
相似題 — LC 82 (下方,一列指標) · LC 61 (同樣的間隔找出旋轉後的新尾巴)
Insertion Sort List — LC 147
關鍵 — 虛擬節點是已排序前綴的頭,所以把節點插到目前最小值之前, 跟插到任何其他位置是同一個接線動作。每拆下一個節點,就讓
p從虛擬節點走到 最後一個比它小的節點,再把它接在p之後。
歸約到:linked_list § Template 2
— 虛擬節點站在一條正在建立的串列的 head 之前,而對 p.next 的前瞻找的是插入點,不是命中。
# python
# LC 147 - Insertion Sort List
# IDEA: detach each node and splice it into a dummy-headed sorted list after the last smaller node
# time = O(n^2), space = O(1)
class Solution(object):
def insertionSortList(self, head):
dummy = ListNode(0) # sorted part: dummy.next onward
cur = head
while cur:
nxt = cur.next # detach cur: cache the rest first
p = dummy
while p.next and p.next.val < cur.val:
p = p.next # p = last sorted node smaller than cur
cur.next = p.next # splice cur after p
p.next = cur
cur = nxt
return dummy.next
// java
// LC 147 - Insertion Sort List
// IDEA: detach each node and splice it into a dummy-headed sorted list after the last smaller node
// time = O(n^2), space = O(1)
public ListNode insertionSortList(ListNode head) {
ListNode dummy = new ListNode(0);
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
ListNode p = dummy;
while (p.next != null && p.next.val < cur.val) {
p = p.next;
}
cur.next = p.next;
p.next = cur;
cur = next;
}
return dummy.next;
}
陷阱
- 接線前忘了
nxt = cur.next:cur.next = p.next會改掉它,[4,2,1,3]未排序的剩餘部分在第一次插入後就弄丟了。 - 先設
dummy.next = head:已排序部分必須從空的開始,否則第一個節點會跟自己比較,串列形成環。
不適用時 — 追問要求 O(n log n):那是串列上的合併排序,LC 148,用 Template 5 的第一個中點來切。
Remove Zero Sum Consecutive Nodes from Linked List — LC 1171
關鍵 — 兩個相同的前綴和,代表中間的節點加總為 0。記下每個前綴和最後 一次出現的節點,再走一趟,從每個節點直接跳到那個最後出現節點的下一個: 中間每一段零和區段一次跳過。
歸約到:linked_list § Template 2 — 虛擬節點(前綴 0)讓從 head 開始的零和區段也能被移除 — 再結合 hash_map § Template 4 的前綴和表,只是這裡存的是節點而不是次數。
# python
# LC 1171 - Remove Zero Sum Consecutive Nodes from Linked List
# IDEA: prefix sum -> last node reaching it; second pass jumps from each prefix to its last occurrence
# time = O(n), space = O(n)
class Solution(object):
def removeZeroSumSublists(self, head):
dummy = ListNode(0, head)
# pass 1: remember the LAST node reaching each prefix sum
last_seen = {}
prefix, cur = 0, dummy
while cur:
prefix += cur.val
last_seen[prefix] = cur # overwrite -> keeps the last one
cur = cur.next
# pass 2: from the first node with prefix p, jump past the last node with prefix p
prefix, cur = 0, dummy
while cur:
prefix += cur.val
cur.next = last_seen[prefix].next
cur = cur.next
return dummy.next
// java
// LC 1171 - Remove Zero Sum Consecutive Nodes from Linked List
// IDEA: prefix sum -> last node reaching it; second pass jumps from each prefix to its last occurrence
// time = O(n), space = O(n)
public ListNode removeZeroSumSublists(ListNode head) {
ListNode dummy = new ListNode(0);
dummy.next = head;
Map<Integer, ListNode> lastSeen = new HashMap<>();
int prefix = 0;
for (ListNode cur = dummy; cur != null; cur = cur.next) {
prefix += cur.val;
lastSeen.put(prefix, cur);
}
prefix = 0;
for (ListNode cur = dummy; cur != null; cur = cur.next) {
prefix += cur.val;
cur.next = lastSeen.get(prefix).next;
}
return dummy.next;
}
試跑([1,2,-3,3,1])
node : dummy 1 2 -3 3 1
prefix : 0 1 3 0 3 4
last : prefix 0 -> node -3, prefix 3 -> node 3
pass 2 : dummy (0) jumps past -3 -> dummy -> 3
3 (3) jumps past 3 -> 3 -> 1
result : [3,1]
陷阱
- 前綴和從
head開始,而不是從值為 0 的虛擬節點開始:[1,-1]應該回傳[],但 head 之前沒有記錄前綴 0,結果回傳[-1]。 - 每個前綴只記第一個節點而不是最後一個:在第二次出現時,跳躍會指回區段的開頭,於是
[1,-1]變成環,第二趟永遠不會結束。
兩個虛擬節點,先拆再接題目
Merge Two Sorted Lists — LC 21
關鍵 — 一個虛擬節點、一個尾指標、兩個輸入:把較小的 head 接到尾巴上, 並讓那個輸入前進。一個輸入用完時,另一個本來就已排序, 所以
tail.next = l1 or l2以 O(1) 收尾。
歸約到:linked_list § Template 3 — 反過來跑:兩條鏈進、一條鏈出,由比較決定下一個節點由哪個輸入提供。
# python
# LC 21 - Merge Two Sorted Lists
# IDEA: dummy + tail; splice the smaller head each step, then hang the leftover list on the tail
# time = O(m + n), space = O(1)
class Solution(object):
def mergeTwoLists(self, l1, l2):
dummy = ListNode(0)
tail = dummy # last node of the merged list
while l1 and l2:
if l1.val <= l2.val: # <= keeps equal values in input order
tail.next = l1 # splice the node itself, not a copy of its value
l1 = l1.next
else:
tail.next = l2
l2 = l2.next
tail = tail.next
tail.next = l1 or l2 # one list is empty; the other is already sorted
return dummy.next
陷阱
- 寫成
tail = l1,而不是tail.next = l1再tail = tail.next:什麼都沒接上,[1,2,4]+[1,3,4]回傳[]。 - 把值複製成新節點也行,但多花了題目不需要的 O(m + n) 空間。
不適用時 — 面試官要遞迴寫法:見 recursion.md § 4-3,O(m + n) 呼叫堆疊。k 條串列見 LC 23。
Split Linked List in Parts — LC 725
關鍵 — n 個節點時,第 i 段有
n // k個節點,i < n % k時再多一個, 所以較長的段在前面。從目前的 head 走那麼多個節點,在最後一個之後切開; 大小為 0 的段是None。
歸約到:linked_list § Template 3
— k 條鏈而不是兩條,依數量而不是依條件切,每條都用 .next = None 收尾,就像第二條鏈的尾巴一樣。
# python
# LC 725 - Split Linked List in Parts
# IDEA: count n; part i gets n // k nodes, plus one when i < n % k; walk size - 1 nodes, then cut
# time = O(n + k), space = O(k) for the output
class Solution(object):
def splitListToParts(self, head, k):
length = 0
node = head
while node:
length += 1
node = node.next
base, extra = divmod(length, k)
parts = []
cur = head
for i in range(k):
parts.append(cur) # None once the nodes run out
size = base + (1 if i < extra else 0)
if size == 0:
continue
for _ in range(size - 1): # stand on the part's last node
cur = cur.next
nxt = cur.next
cur.next = None # cut: terminate this part
cur = nxt
return parts
陷阱
- 把多出來的節點分給最後幾段:
[1..10]、k = 3,必須是[1,2,3,4]、[5,6,7]、[8,9,10];前面的段比較長。 - 漏掉切斷:每一段都一路連到原串列的結尾。
Linked List Components — LC 817
關鍵 — 一個連通分量是值都在
nums裡的一段最長連續節點。數這些段的結尾: 一個在集合裡、而下一個節點不存在或不在集合裡的節點, 正好結束一個分量。
歸約到:linked_list § Template 3 — 集合成員判斷把節點切成一段段,就像 Template 3 的條件把節點切成兩條鏈,只是這些段只被計數,從不被建立。
# python
# LC 817 - Linked List Components
# IDEA: a node in the set whose next is absent or outside the set ends one component
# time = O(n + len(nums)), space = O(len(nums))
class Solution(object):
def numComponents(self, head, nums):
in_set = set(nums)
count = 0
while head:
run_ends = head.next is None or head.next.val not in in_set
if head.val in in_set and run_ends:
count += 1
head = head.next
return count
// java
// LC 817 - Linked List Components
// IDEA: a node in the set whose next is absent or outside the set ends one component
// time = O(n + nums.length), space = O(nums.length)
public int numComponents(ListNode head, int[] nums) {
Set<Integer> inSet = new HashSet<>();
for (int num : nums) {
inSet.add(num);
}
int count = 0;
for (ListNode cur = head; cur != null; cur = cur.next) {
boolean runEnds = cur.next == null || !inSet.contains(cur.next.val);
if (inSet.contains(cur.val) && runEnds) {
count++;
}
}
return count;
}
陷阱
- 把集合裡的每個節點都算進去:
[0,1,2,3]、nums =[0,1,3],有 3 個這樣的節點,但只有 2 個分量。 - 用串列而不是集合做成員判斷,每次判斷就變成 O(len(nums))。
| LC | 題目 | 歸約到 | 難度 | 位置 |
|---|---|---|---|---|
| 23 | Merge k Sorted Lists | heap § Template 3 | Hard | 詳寫於 heap.md |
反轉一段題目
Swap Nodes in Pairs — LC 24
關鍵 — 在 k = 2 時,反轉一段的輔助函式的三個把手變成區域變數:
second是 新的前端、first是新的尾巴、second.next是之後的節點。prev是唯一的 游標:下一對的兩個節點都能從它走到。
歸約到:linked_list § Template 4 — k = 2 的情況,展開來寫,因為反轉兩個節點只是兩行賦值。
| 輔助函式回傳 | 在 k = 2 時就是 |
實際寫成 |
|---|---|---|
new_head |
second |
prev.next = second |
new_tail |
first |
prev = first — 下一輪的 prev |
next_node |
second.next |
直接折進 first.next = second.next |
# python
# LC 24 - Swap Nodes in Pairs
# IDEA: the k = 2 case of the segment helper, unrolled; prev is the only cursor
# time = O(n), space = O(1)
class Solution(object):
def swapPairs(self, head):
dummy = ListNode(0)
dummy.next = head
prev = dummy # node BEFORE the pair
while prev.next and prev.next.next:
first = prev.next # becomes the pair's tail (new_tail)
second = first.next # becomes the pair's front (new_head)
first.next = second.next # (A) carry next_node straight into the new tail
second.next = first # (B) flip the pair
prev.next = second # (C) prev adopts the new front
prev = first # new_tail sits before the next pair
return dummy.next
試跑([1,2,3,4])
dummy -> 1 -> 2 -> 3 -> 4 prev = dummy, first = 1, second = 2
(A) first.next = second.next 1 -> 3
(B) second.next = first 2 -> 1
(C) prev.next = second dummy -> 2 -> 1 -> 3 -> 4
prev = first (1) next pair is 1.next, 1.next.next = (3, 4)
... same three links on (3, 4) dummy -> 2 -> 1 -> 4 -> 3
prev.next.next is None -> stop return [2,1,4,3]
陷阱
- 把
(B)放在(A)之前會先蓋掉second.next,於是(A)讓first指向自己:[1,2,3,4]留下1 -> 1,迴圈永遠不會結束。 - 讓
prev移到second而不是first:交換後second是這一對的前端,所以下一「對」橫跨了兩對,[1,2,3,4]回傳[2,3,4,1]。 []、[1]或奇數長度的尾巴都不需要防呆:[1,2,3]交換一次,留下3。
不適用時 — 面試官要遞迴寫法(O(n) 堆疊):詳寫於 recursion.md § 5-1。
Flatten a Multilevel Doubly Linked List — LC 430 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 把子鏈接進來就是一次「接回一段」:兩個接縫,
cur <-> child和tail <-> next,兩個方向都要設。之後繼續往前走:接進來的鏈會是下一個被走訪的, 所以更深的層級不用遞迴也不用堆疊就會被攤平。
歸約到:linked_list § Template 4 — 接入而不是反轉:那一段(子鏈)整段放進去,只剩兩次接線,而且是在雙向鏈結串列上。
# python
# LC 430 - Flatten a Multilevel Doubly Linked List
# IDEA: in-place splice: insert the child chain between cur and cur.next, then keep walking
# time = O(n), space = O(1) each chain is tail-scanned once
class Solution(object):
def flatten(self, head):
cur = head
while cur:
if cur.child:
nxt = cur.next # save the successor
child = cur.child
cur.child = None # no child pointer may survive
cur.next = child # seam 1: cur <-> child
child.prev = cur
tail = child # last node of the child chain
while tail.next:
tail = tail.next
tail.next = nxt # seam 2: tail <-> nxt
if nxt:
nxt.prev = tail
cur = cur.next # walks INTO the spliced child
return head
// java
// LC 430 - Flatten a Multilevel Doubly Linked List
// IDEA: in-place splice: insert the child chain between cur and cur.next, then keep walking
// time = O(n), space = O(1) each chain is tail-scanned once
public Node flatten(Node head) {
Node cur = head;
while (cur != null) {
if (cur.child != null) {
Node next = cur.next;
Node child = cur.child;
cur.child = null;
cur.next = child;
child.prev = cur;
Node tail = child;
while (tail.next != null) {
tail = tail.next;
}
tail.next = next;
if (next != null) {
next.prev = tail;
}
}
cur = cur.next;
}
return head;
}
試跑
1 <-> 2 <-> 3 <-> 4
|
7 <-> 8 <-> 9
|
11 <-> 12
at node 3: 1 <-> 2 <-> 3 <-> 7 <-> 8 <-> 9 <-> 4
at node 8: 1 <-> 2 <-> 3 <-> 7 <-> 8 <-> 11 <-> 12 <-> 9 <-> 4
陷阱
- 沒把
cur.child清掉:攤平後仍留有 child 指標的串列會被判錯。 - 接縫只設了
next那一側:從4往回走就會跳過子鏈。 - 最後一個節點有 child:
nxt是None,所以nxt.prev = tail必須先檢查。
相似題 — LC 114 (同樣的接入,用在樹上:把左子樹掛在 root 和 right 之間)
Reverse Nodes in k-Group — LC 25
關鍵 — 每一組做完後,這組的舊 head 就是它的新 tail,而那正是下一組的入邊 必須掛上去的節點:
prev = new_tail。反轉之前先數 k 個節點, 不足 k 個的最後一組就保持原狀。
歸約到:linked_list § Template 4 — 同一個三把手輔助函式,每個滿 k 個的組呼叫一次,而不是只呼叫一次。
# python
# LC 25 - Reverse Nodes in k-Group
# IDEA: while k nodes remain after prev, reverse them with the helper and reconnect;
# a final group shorter than k is left as it is
# time = O(n), space = O(1)
class Solution(object):
def reverseKGroup(self, head, k):
dummy = ListNode(0)
dummy.next = head
prev = dummy # node before the current group
while self.has_k(prev.next, k):
new_head, new_tail, next_node = self.reverse_helper(prev.next, k)
prev.next = new_head # front of the group
new_tail.next = next_node # tail of the group -> rest
prev = new_tail # the next group hangs off this tail
return dummy.next
def has_k(self, node, k):
# are there at least k nodes starting at node?
count = 0
while node and count < k:
node = node.next
count += 1
return count == k
def reverse_helper(self, head, k):
# reverse k nodes from head; return (new_head, new_tail, first node after)
prev = None
curr = head
while curr and k > 0:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
k -= 1
return prev, head, curr
試跑([1,2,3,4,5]、k = 2)
group 1: reverse 1,2 dummy -> 2 -> 1 -> 3 -> 4 -> 5 prev = 1
group 2: reverse 3,4 dummy -> 2 -> 1 -> 4 -> 3 -> 5 prev = 3
has_k(5, 2) is False the short group [5] stays as it is
return [2,1,4,3,5]
陷阱
- 不用
has_k就反轉:輔助函式也會把不足 k 個的最後一組反轉,所以[1,2,3,4,5]、k = 3,回傳[3,2,1,5,4],而不是[3,2,1,4,5]。 - 設成
prev = new_head:下一組被掛在這一組的前端而不是尾巴,[1,2,3,4,5]、k = 2,回傳[2,3,4,5,1]。
不適用時 — 面試官要遞迴寫法(O(n / k) 堆疊):詳寫於 recursion.md § 5-1。
| LC | 題目 | 歸約到 | 難度 | 位置 |
|---|---|---|---|---|
| 92 | Reverse Linked List II | linked_list § Template 4 | Medium | 錨點題,詳寫於 linked_list.md |
快慢指標找中點題目
這個模板的題目都在別的文件詳寫;列在這裡,是因為讀完 Template 5 的人會到這一節找它們。
| LC | 題目 | 歸約到 | 難度 | 位置 |
|---|---|---|---|---|
| 160 | Intersection of Two Linked Lists | 2_pointers_linkedlist § 1-1-5 | Easy | 詳寫於 2_pointers_linkedlist.md |
| 234 | Palindrome Linked List | linked_list § Template 5 + Template 1 | Easy | 詳寫於 2_pointers_linkedlist § 1-1-6 |
| 142 | Linked List Cycle II | 2_pointers_linkedlist § 1-1-2 | Medium | 詳寫於 2_pointers_linkedlist.md |
| 148 | Sort List | linked_list § Template 5 + LC 21 的合併 | Medium | 詳寫於 advanced_divide_and_conquer § LC 148 |
進位迴圈題目
Plus One Linked List — LC 369
關鍵 — 進位從最低位開始,而最低位是串列的尾巴。 先反轉串列,讓進位迴圈能往前走;進位一變成 0 就停,進位撐過最後一位就補一個節點, 最後再反轉回來。
歸約到:linked_list § Template 6 — 一個輸入、起始進位為 1,外面包兩次 Template 1 反轉,因為位數是最高位在前。
# python
# LC 369 - Plus One Linked List
# IDEA: reverse, add a carry of 1 until it dies (appending a node if it survives), reverse back
# time = O(n), space = O(1)
class Solution(object):
def plusOne(self, head):
head = self.reverse(head)
cur = head
carry = 1
while cur and carry:
total = cur.val + carry
cur.val = total % 10
carry = total // 10
if carry and cur.next is None: # the carry outlives the last digit
cur.next = ListNode(carry)
carry = 0
cur = cur.next
return self.reverse(head)
def reverse(self, head):
prev = None
while head:
nxt = head.next
head.next = prev
prev = head
head = nxt
return prev
// java
// LC 369 - Plus One Linked List
// IDEA: reverse, add a carry of 1 until it dies (appending a node if it survives), reverse back
// time = O(n), space = O(1)
public ListNode plusOne(ListNode head) {
head = reverse(head);
ListNode cur = head;
int carry = 1;
while (cur != null && carry > 0) {
int total = cur.val + carry;
cur.val = total % 10;
carry = total / 10;
if (carry > 0 && cur.next == null) {
cur.next = new ListNode(carry);
carry = 0;
}
cur = cur.next;
}
return reverse(head);
}
private ListNode reverse(ListNode head) {
ListNode prev = null;
while (head != null) {
ListNode next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
陷阱
- 漏掉最後的進位:
[9,9]必須變成[1,0,0];沒有補上的節點就是[0,0]。 - 把串列轉成整數在 Python 可行,但在 Java 上長串列會讓
long溢位。
相似題 — LC 2 (錨點題:兩個輸入,位數已經是最低位在前) · LC 445 (最高位在前,兩個輸入)
雜湊表 + 雙向鏈結串列題目
Copy List with Random Pointer — LC 138
關鍵 —
random指標可能指向還沒被複製的節點,所以分兩趟複製: 先把每個舊節點對應到一個新副本,再透過對照表接好每個副本的next和random。map.get(None)回傳None,所以串列尾端不需要分支。
歸約到:hash_map § Template 8 — 舊到新的節點對照表,用在一條因多出一個指標而變成圖的串列上。
# python
# LC 138 - Copy List with Random Pointer
# IDEA: pass 1 maps every old node to a new node; pass 2 wires next and random through the map
# time = O(n), space = O(n)
class Solution(object):
def copyRandomList(self, head):
old_to_new = {}
cur = head
while cur: # pass 1: create every copy
old_to_new[cur] = Node(cur.val)
cur = cur.next
cur = head
while cur: # pass 2: wire the copies
old_to_new[cur].next = old_to_new.get(cur.next) # None -> None
old_to_new[cur].random = old_to_new.get(cur.random)
cur = cur.next
return old_to_new.get(head)
// java
// LC 138 - Copy List with Random Pointer
// IDEA: pass 1 maps every old node to a new node; pass 2 wires next and random through the map
// time = O(n), space = O(n)
public Node copyRandomList(Node head) {
Map<Node, Node> oldToNew = new HashMap<>();
for (Node cur = head; cur != null; cur = cur.next) {
oldToNew.put(cur, new Node(cur.val));
}
for (Node cur = head; cur != null; cur = cur.next) {
Node copy = oldToNew.get(cur);
copy.next = oldToNew.get(cur.next); // get(null) returns null
copy.random = oldToNew.get(cur.random);
}
return oldToNew.get(head);
}
陷阱
- 在第一趟就接
random:random可能指向前方、還沒有副本的節點。 - 把副本的
random指向舊節點(cur.random)而不是它的副本:印出來看起來沒錯,但跟輸入共用節點,會被判錯。
不適用時 — 追問要求 O(1) 額外空間:把每個副本接在原節點之後(A -> A' -> B -> B'),設 A'.random = A.random.next,再拆開。
| LC | 題目 | 歸約到 | 難度 | 位置 |
|---|---|---|---|---|
| 146 | LRU Cache | linked_list § Template 7 | Medium | 錨點題,詳寫於 linked_list.md |
| 460 | LFU Cache | linked_list § Template 7,LFU 變體 | Hard | 詳寫於 linked_list.md |