鏈結串列 — 實戰題解

鏈結串列優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值 更新於 Nov 12, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 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
# 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
// 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])

text
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
# 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
// 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])

text
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
# 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
// 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),也就是虛擬節點存在的理由:

text
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
# 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
// 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
# 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
// 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])

text
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
# 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
# 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
# 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
// 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
# 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])

text
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
# 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
// 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;
}

試跑

text
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
# 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)

text
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
# 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
// 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
# 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
// 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