陣列

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

範圍 — 陣列的基本功——原地改寫、旋轉、分割,以及「用索引當雜湊」的招式。它擁有這些操作本身;至於各個模式家族(視窗、指標、前綴和)各有自己的檔案。 另見:array_examples.md — 撐起這些操作的題目詳解;python_trick.md 與 java_trick_strings_sorting.md — 排序鍵與比較器,多鍵排序規則歸它們管;2_pointers.md、sliding_window.md、prefix_sum.md 和 difference_array.md — 四大陣列模式家族;matrix.md — 二維;sort.md — 排序。

最基本的線性資料結構

LeetCode 題目清單

時間複雜度

資料結構 Search Insert Delete Min/Max
陣列 O(n) O(n) O(n) O(n)

這裡指未排序的動態陣列。用索引存取是 O(1);append 是攤還 O(1);任意位置的 Insert/Delete 是 O(n)(要搬移元素)。陣列若已排序,Search 可以降到 O(log n)(二分搜尋)。

0) 概念

  • Java Array
    • 底層:記憶體中一塊連續的空間

0-1) 分類

0-2) 辨識

多數「陣列」題其實會被導到某個模式文件(見 2) 模式選擇)。 下面這些訊號才是留在這份文件的。限制條件的提示依照 complexity_cheatsheet.md 裡的尺規。

題目敘述裡的訊號 限制條件提示 該用 不是這個,如果
「把 nums2 原地合併進 nums1」、「最後 n 格是 0,應該忽略」 追問 O(1) 額外空間 2_pointers § Template 5 反向寫入指標 空位在前面(移除重複)→ 正向快慢指標,2_pointers.md
「insert / remove / getRandom 都要平均 O(1)」 約 10⁵ 次呼叫 Template 7 和最後一個交換再刪除 允許重複(LC 381)→ 值對應到一組索引的集合
「出現超過 ⌊n/2⌋ 次」、「O(1) 額外空間」 n ≤ 5·10⁴,一次掃描 Template 2 多數投票 「最常出現」/「前 k 個」→ 需要計數,hash_map.md
「所有出現超過 ⌊n/3⌋ 次的元素」 O(1) 空間 Template 3 k − 1 個候選
「1..n 的排列」、「對每個前綴,有幾個 …」 值 ≤ n → 陣列本身就是雜湊表 Template 4 頻率陣列+即時累計 值可到 10⁹ → 用雜湊表,做同樣的「跨過門檻」判斷
「所有子字串/子陣列上 X 的總和」 n ≤ 10⁵ → 無法列出 O(n²) 個子陣列 Template 5 (i + 1) * (n - i) 元素只有在是最小/最大值時才算(LC 907)→ monotonic_stack.md
「讓一整列的元素都相等,求最少操作數,否則 −1」 每個元素只有約 2 種選擇 Template 8 從索引 0 取候選 目標可以是任何值 → 排序/中位數,sort.md
在二維陣列上「設計一個迭代器」,next() / hasNext() 每次呼叫攤銷 O(1) Template 1 兩個游標 巢狀深度不固定(LC 341)→ 用迭代器的堆疊,iterator.md
「值在 [1, n] 之間」、「缺少的/重複的/第一個缺少的正數」、「O(1) 額外空間」 n ≤ 10⁵,一到兩次掃描 Template 9 循環排序或正負號標記 不准修改輸入(LC 287)→ 在值域上二分搜尋,binary_search_on_answer.md
「原地往右旋轉 k 步」、兩個區塊互換位置 k 可能大於 n Template 10 三次反轉 是鏈結串列(LC 61)→ 重新接上新的尾巴
「到最近的 … 的最大距離」、「有幾個前綴 …」、答案只取決於前綴的一個摘要 一次掃描,O(1) 空間 Template 11 即時狀態 摘要被打敗時要丟掉 → monotonic_stack.md

1) 一般形式

1-1) 基本操作

1-1-0) 切割陣列

python
# python
# itertools.islice(seq, [start,] stop [, step]) slices any iterator without building a list
import itertools

x = itertools.islice(range(10), 0, 9, 2)
print(list(x))   # -> [0, 2, 4, 6, 8]

y = itertools.islice(range(10), 0, 10, 3)
print(list(y))   # -> [0, 3, 6, 9]

1-1-1) 插入元素

python
# python
p = [[7, 0], [7, 1], [6, 1], [5, 0], [5, 2], [4, 4]]
p.insert(1, [6, 1])   # O(n): shifts everything after index 1
print(p)   # -> [[7, 0], [6, 1], [7, 1], [6, 1], [5, 0], [5, 2], [4, 4]]

1-1-2) 刪除元素

python
# python
p = [[7, 0], [7, 1], [6, 1], [5, 0], [5, 2], [4, 4]]
p.remove([7, 1])      # O(n): removes the FIRST equal element; ValueError if absent
print(p)   # -> [[7, 0], [6, 1], [5, 0], [5, 2], [4, 4]]

1-1-3) 檢查元素是否在陣列中

python
# python
p = [[7, 0], [6, 1], [5, 0], [5, 2], [4, 4]]
print([7, 0] in p)    # -> True   (O(n) scan; use a set for repeated lookups)

1-1-4) 加到陣列(頭、尾)

python
# python
p = [[7, 0], [6, 1], [5, 0], [5, 2], [4, 4]]
p.append([0, 0])      # tail: O(1) amortized
print(p)   # -> [[7, 0], [6, 1], [5, 0], [5, 2], [4, 4], [0, 0]]
p.insert(0, [9, 9])   # head: O(n); use collections.deque.appendleft for O(1)
print(p[0])   # -> [9, 9]

1-1-5) 排序陣列*****

list.sort() 是原地排序,回傳 None。sorted() 回傳一個新的 list。

nums.sort(...) sorted(nums, ...)
原地? ✅ 是 — nums 會被改動 ❌ 否 — 原本的不動
回傳值 None 新的已排序 list
可用對象 只有 list 任何 iterable(str、tuple、set、dict、generator)
空間 額外 O(1)* O(n)
穩定? ✅ 是(Timsort) ✅ 是(Timsort)

* CPython 的 Timsort 最壞情況可能需要一塊 O(n) 的暫存區,但不會配置新的 list 物件。

python
# python
# IDEA: list.sort(key=...) reorders the list itself; sorted(iterable, key=...) builds a new list
# time = O(n log n), space = O(n) for sorted(), no new list for .sort()
strs = ["eat", "tea", "tan", "ate", "nat", "bat"]

# in place: sort by each word's letters (the LC 49 grouping key); stable, so ties keep input order
strs.sort(key=lambda word: "".join(sorted(word)))
print(strs)        # -> ['bat', 'eat', 'tea', 'ate', 'tan', 'nat']

# a new list: the original keeps its order
nums = [3, 1, 2]
ascending = sorted(nums)
descending = sorted(nums, reverse=True)
print(nums, ascending, descending)   # -> [3, 1, 2] [1, 2, 3] [3, 2, 1]

常見錯誤

python
# python
# 1) assigning the result of an in-place sort: .sort() returns None
arr = [3, 1, 2]
result = arr.sort()
print(result, arr)        # -> None [1, 2, 3]   (so arr = arr.sort() loses the list)

# 2) .sort() exists only on lists
s = "cba"
try:
    s.sort()
except AttributeError as err:
    print(err)            # -> 'str' object has no attribute 'sort'
print("".join(sorted(s)))  # -> abc

# 3) a dict or a set: only sorted() works, and a dict sorts its KEYS
print(sorted({"b": 2, "a": 1}))   # -> ['a', 'b']
print(sorted({3, 1, 2}))          # -> [1, 2, 3]

# 4) sorting the caller's list inside a helper is a side effect
def smallest_two(nums):
    return sorted(nums)[:2]       # sorts a copy: the caller's list keeps its order

original = [5, 1, 4, 2]
print(smallest_two(original), original)   # -> [1, 2] [5, 1, 4, 2]

什麼時候用哪一個

  • .sort() → list 是你的、而且想省空間到 O(1)(例如 LC 406、LC 56 Merge Intervals、LC 253)
  • sorted() → 輸入是字串/dict/set/tuple,或原本的順序必須保留

Java 的對應寫法:

java
// java
// in place (like Python's list.sort())
int[] arr = {3, 1, 2};
Arrays.sort(arr);                                    // primitive array -> [1, 2, 3]
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
Collections.sort(list);                              // List, in place -> [1, 2, 3]
List<int[]> pairs = new ArrayList<>();
pairs.add(new int[]{2, 0});
pairs.add(new int[]{1, 5});
pairs.sort((a, b) -> Integer.compare(a[0], b[0]));   // List + comparator, in place

// a NEW collection (like Python's sorted())
List<Integer> source = Arrays.asList(3, 1, 2);
List<Integer> sortedCopy = source.stream().sorted().collect(Collectors.toList());  // source untouched

1-1-6) 攤平陣列

攤平巢狀 list 就是一次深度優先走訪:整數是葉子,直接輸出;list 是節點,依序走訪它的子元素。 Python 沒有能處理任意深度的內建函式。

python
# python
# IDEA: depth-first walk - emit an int, recurse into a list, keep the left-to-right order
# time = O(N + L), space = O(D)   N = ints, L = lists, D = nesting depth (the recursion stack)
def flatten(nested):
    out = []

    def walk(items):
        for item in items:
            if isinstance(item, list):
                walk(item)            # a list: descend
            else:
                out.append(item)      # a leaf: emit

    walk(nested)
    return out


print(flatten([1, 0, [1, 2, [4, [5, [6, [7]]]]]]))   # -> [1, 0, 1, 2, 4, 5, 6, 7]
print(flatten([[1, 1], 2, [1, 1]]))                  # -> [1, 1, 2, 1, 1]

第二個版本留著是有理由的:同樣的走訪寫成 generator 就是惰性的。事前什麼都不建,呼叫端提早停下時, 剩下的部分也不會被走到。這正是下面 Template 1 替迭代器類別做的「急切或惰性」選擇。

python
# python
# IDEA: the same walk as a generator - values are produced on demand
# time = O(N + L) over a full iteration, space = O(D)
def iter_flat(nested):
    for item in nested:
        if isinstance(item, list):
            yield from iter_flat(item)
        else:
            yield item


print(list(iter_flat([1, [4, [6]]])))   # -> [1, 4, 6]

LeetCode 有兩種設計把這個走訪變成迭代器類別:

  • 深度固定為二(LC 251 Flatten 2D Vector):用兩個游標取代遞迴,就是下面的 Template 1;
  • 深度不固定(LC 341 Flatten Nested List Iterator):用一個待處理項目的堆疊取代遞迴,在 hasNext() 裡惰性地拆開。詳解在 iterator.md § 2-2。
Template 1: Lazy Two-Cursor Iterator — LC 251 ⭐⭐⭐⭐

辨識 — 在二維或巢狀陣列上「設計一個迭代器」/「實作 next() 和 hasNext()」,常會追問 「輸入非常大」或「只用 O(1) 額外空間」。呼叫次數 ≤ 10⁵ → 每次呼叫必須攤銷 O(1)。

關鍵 — (row, col) 永遠指向原本的陣列,而且 hasNext() 回傳之後它一定停在一個真正的 元素上;只有 next() 會越過元素。所以什麼都不用複製,而每一列在整趟走訪中只會被跳過一次。

急切式(在建構子裡攤平) 惰性(兩個游標)
建構子 O(N):複製每個元素 O(1)
額外空間 O(N) O(1)
next() / hasNext() O(1) 攤銷 O(1):整趟走訪中每一列只被跳過一次,總共 O(R + N)
來源很大或會變動時 不行:它是快照 可以:沒有預先讀取任何東西

N = 元素總數,R = 列數。急切式版本就是上面的 flatten:在建構子裡執行,next() 讀存好的 list。 面試官說「設計一個迭代器」指的是惰性版本,它也是 iterator.md 裡 hasNext() / next() 約定 的二維特例: 深度固定為二,所以兩個索引就能取代堆疊。

python
# python
# LC 251 - Flatten 2D Vector
# IDEA: lazy iterator - two cursors (row, col) into the ORIGINAL 2D array;
#       hasNext() skips exhausted / empty rows, next() reads one element and steps col
# time = O(1) amortized per call (O(R + N) over a full pass), space = O(1) extra
class Vector2D:

    def __init__(self, vec):
        self.vec = vec      # no copy: iterate the input where it is
        self.row = 0        # which inner list
        self.col = 0        # which index inside vec[row]

    def hasNext(self):
        # normalize: move (row, col) onto the next real element, if there is one.
        # re-entrant -> a second call finds col already valid and returns at once
        while self.row < len(self.vec):
            if self.col < len(self.vec[self.row]):
                return True
            # this row is used up (or was empty from the start): go to the next one
            self.row += 1
            self.col = 0
        return False

    def next(self):
        # normalize first, so next() is correct even if the caller never called hasNext()
        if not self.hasNext():
            raise StopIteration
        val = self.vec[self.row][self.col]
        self.col += 1
        return val
java
// java
// LC 251 - Flatten 2D Vector
// IDEA: same two cursors; hasNext() normalizes, next() is the only method that consumes
// time = O(1) amortized per call, space = O(1) extra
class Vector2D {
    private final int[][] vec;
    private int row = 0;   // which inner array
    private int col = 0;   // which index inside vec[row]

    public Vector2D(int[][] vec) {
        this.vec = vec;
    }

    public boolean hasNext() {
        // skip every used-up or empty row until (row, col) points at an element
        while (row < vec.length) {
            if (col < vec[row].length) {
                return true;
            }
            row++;
            col = 0;
        }
        return false;
    }

    public int next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        int val = vec[row][col];
        col++;
        return val;
    }
}

複雜度

  • 時間:每次呼叫攤銷 O(1):hasNext() 裡的 while 可能一次跳過好幾列,但 row 只會往前, 所以整趟走訪中它總共只前進 R 次:O(R + N)。
  • 空間:O(1) 額外:self.vec = vec 存的是參考而不是複本;狀態只有兩個整數。

試跑

text
vec = [[1, 2], [], [3]]          (row, col) after each call

next()     -> 1                  (0, 1)
next()     -> 2                  (0, 2)   row 0 is used up, but nothing moves yet
hasNext()  -> true               (2, 0)   skipped row 0 (used up) and row 1 (empty)
hasNext()  -> true               (2, 0)   idempotent: already on an element
next()     -> 3                  (2, 1)
hasNext()  -> false              (3, 0)   row == len(vec)

陷阱

  • next() 盲信游標。 self.col += 1; return vec[row][col - 1] 只有在先呼叫過 hasNext() 時才正確。在 [[], [3]] 上直接呼叫 next() 會讀到空的第 0 列而崩潰。在 next() 裡先正規化 不花任何成本,就能消除這個依賴。
  • hasNext() 會消耗元素。 它可以跳過什麼都沒有的列,但絕不能越過真正的元素;只有 next() 可以。
  • 該用 while 卻用了 if。 連續兩個空列([[1], [], [], [2]])需要跳不只一次。
  • 換列時忘了 col = 0:在 [[1, 2], [3]] 上,第二列會從索引 2 開始讀而被整列跳過。

不適用時 — 巢狀深度不固定(LC 341 Flatten Nested List Iterator):兩個游標追蹤不了 無上限的深度,改用迭代器的堆疊 (iterator.md)。

練習 — LC 251 ⭐ · LC 284 (peek:先快取下一個元素) · LC 281 (zigzag:游標的佇列) · LC 341 (深度不固定:游標的堆疊)

1-1-7) 同時走訪兩個陣列(長度可能不同)

走到較長的那個長度,把超出範圍的位置當成預設值讀,這樣兩個陣列都不必先補齊、複製或交換。 LC 165 Compare Version Numbers 就是在兩串版本號上做這種走訪,缺少的版本號算 0。

python
# python
# IDEA: loop to max(len); an index past the end of one array reads as the default (0 here)
# time = O(max(m, n)), space = O(1)
from itertools import zip_longest

s = [1, 2, 3, 4]
t = [1, 2]

for i in range(max(len(s), len(t))):
    a = s[i] if i < len(s) else 0
    b = t[i] if i < len(t) else 0
    print(i, a, b)
# -> 0 1 1
#    1 2 2
#    2 3 0
#    3 4 0

# the same walk with the standard library
print(list(zip_longest(s, t, fillvalue=0)))   # -> [(1, 1), (2, 2), (3, 0), (4, 0)]

# only the common prefix: zip stops at the shorter one
print(list(zip(s, t)))                        # -> [(1, 1), (2, 2)]

1-2) 特殊的陣列演算法 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

1-2-1) Boyer-Moore 多數投票演算法

把不同的元素兩兩配對抵銷,就能用 O(1) 額外空間找出出現超過 ⌊n/k⌋ 次的元素。 streaming_algorithms.md 說明了為什麼同樣的想法也適用於資料流。

Template 2: Majority Vote, One Candidate — LC 169

辨識 — 「出現超過 ⌊n/2⌋ 次的元素」、「可以假設多數元素一定存在」,追問「O(1) 額外空間」。 n ≤ 5·10⁴ → 一次 O(n) 掃描。

關鍵 — 每一次減一,都是把一個 candidate 和一個不同的值互相抵銷。出現超過 n/2 次的值 不可能被完全抵銷,所以最後留下的候選就是它。

python
# python
# LC 169 - Majority Element
# IDEA: Boyer-Moore vote - adopt a candidate when count is 0, +1 on a match, -1 otherwise
# time = O(n), space = O(1)
def majorityElement(nums):
    candidate = None
    count = 0
    for num in nums:
        if count == 0:
            candidate = num       # everything before here cancelled out: start fresh
            count = 1
        elif num == candidate:
            count += 1
        else:
            count -= 1            # pair num with one copy of candidate and drop both
    # LC 169 guarantees a majority exists; without that guarantee,
    # verify: return candidate if nums.count(candidate) > len(nums) // 2 else None
    return candidate
java
// java
// LC 169 - Majority Element
// IDEA: Boyer-Moore vote - adopt a candidate when count is 0, +1 on a match, -1 otherwise
// time = O(n), space = O(1)
public int majorityElement(int[] nums) {
    int candidate = nums[0];
    int count = 0;
    for (int num : nums) {
        if (count == 0) {
            candidate = num;      // everything before here cancelled out: start fresh
            count = 1;
        } else if (num == candidate) {
            count++;
        } else {
            count--;              // pair num with one copy of candidate and drop both
        }
    }
    return candidate;             // guaranteed to exist in LC 169; verify otherwise
}

複雜度

  • 時間 O(n):唯一的 for num in nums 迴圈對每個元素做 O(1) 的事(若要驗證,再多一次 O(n) 掃描)。
  • 空間 O(1):全部的狀態就是 candidate 和 count。

試跑

text
nums = [2,2,1,1,1,2,2]
num | candidate | count | action
 2  |     2     |   1   | count was 0 -> adopt
 2  |     2     |   2   | match
 1  |     2     |   1   | cancel
 1  |     2     |   0   | cancel
 1  |     1     |   1   | count was 0 -> adopt
 2  |     1     |   0   | cancel
 2  |     2     |   1   | count was 0 -> adopt   -> answer 2 (4 of 7)

陷阱

  • 沒有保證多數存在時卻省略驗證。 [1,2,3] 最後的候選是 3,但它只出現一次。投票只告訴你: 如果有多數元素,就是它。
  • 候選不是最常出現的元素。 [1,1,2,2,3](沒有多數)最後停在 3,不是 1 或 2。Boyer-Moore 回答的是「> n/2」,從來不是「眾數」。

不適用時 — 門檻是「最常出現」或「前 k 個」,而不是「超過 n/k」:需要真正的計數, 改用雜湊表加桶或堆積(hash_map.md)。

練習 — LC 169 ⭐ · LC 1150 (已排序:改用二分搜尋) · LC 229 (> n/3:Template 3) · LC 1157 (區間內的多數:隨機抽樣或投票線段樹)

Template 3: Majority Vote, k − 1 Candidates — LC 229

辨識 — 「所有出現超過 ⌊n/3⌋ 次(或 ⌊n/k⌋)的元素」,追問「O(1) 空間」。最多只有 k − 1 個值能超過 n/k 的門檻,所以答案是最多 k − 1 個元素的清單。

關鍵 — 和 k − 1 個候選都不同的值,會各抵銷一份,一次移除 k 個值。出現超過 n/k 次的值, 參與抵銷的次數不會超過它自己的個數,所以永遠不會被消滅;但留下來的只是候選,必須再掃一次計數。

python
# python
# LC 229 - Majority Element II
# IDEA: Boyer-Moore with two candidates; a value matching neither cancels one of each;
#       then a second pass verifies both, because survivors are only candidates
# time = O(n), space = O(1)
def majorityElement(nums):
    cand1, cand2 = None, None
    count1, count2 = 0, 0
    for num in nums:
        if num == cand1:              # check both matches BEFORE adopting
            count1 += 1
        elif num == cand2:
            count2 += 1
        elif count1 == 0:
            cand1, count1 = num, 1
        elif count2 == 0:
            cand2, count2 = num, 1
        else:
            count1 -= 1               # num differs from both: cancel three values at once
            count2 -= 1
    result = []
    for cand in (cand1, cand2):       # verify: required here
        if cand is not None and nums.count(cand) > len(nums) // 3:
            result.append(cand)
    return result
java
// java
// LC 229 - Majority Element II
// IDEA: Boyer-Moore with two candidates, then count both again to verify
// time = O(n), space = O(1)
public List<Integer> majorityElement(int[] nums) {
    int cand1 = 0, cand2 = 0;         // safe: a num matching cand1 is never also adopted as cand2
    int count1 = 0, count2 = 0;
    for (int num : nums) {
        if (num == cand1) {
            count1++;
        } else if (num == cand2) {
            count2++;
        } else if (count1 == 0) {
            cand1 = num;
            count1 = 1;
        } else if (count2 == 0) {
            cand2 = num;
            count2 = 1;
        } else {
            count1--;
            count2--;
        }
    }
    count1 = 0;
    count2 = 0;
    for (int num : nums) {            // second pass: survivors are only candidates
        if (num == cand1) {
            count1++;
        } else if (num == cand2) {
            count2++;
        }
    }
    List<Integer> result = new ArrayList<>();
    if (count1 > nums.length / 3) {
        result.add(cand1);
    }
    if (count2 > nums.length / 3) {
        result.add(cand2);
    }
    return result;
}

同一個迴圈可以推廣到任意 k,用一個最多存 k − 1 個候選的 map:

python
# python
# IDEA: generalised Boyer-Moore - at most k-1 candidates; a newcomer that finds the map
#       full decrements every candidate (and drops the ones that reach 0)
# time = O(n * k), space = O(k)
def frequent_over_n_over_k(nums, k):
    candidates = {}                   # value -> surviving count, at most k - 1 keys
    for num in nums:
        if num in candidates:
            candidates[num] += 1
        elif len(candidates) < k - 1:
            candidates[num] = 1
        else:
            for key in list(candidates):
                candidates[key] -= 1
                if candidates[key] == 0:
                    del candidates[key]
    result = []
    for cand in candidates:           # verify each survivor
        if nums.count(cand) > len(nums) // k:
            result.append(cand)
    return result

複雜度

  • 時間 O(n):兩次線性掃描,投票的 for 和驗證的 nums.count(一般化版本是 O(n · k), 因為 map 滿了時要逐個鍵減一)。
  • 空間 O(1):兩個候選和兩個計數器(一般情況 O(k))。

陷阱

  • 省略驗證。 [1,2,3] 留下 1 和 2 兩個候選,但都只出現一次。和 LC 169 不同,LC 229 什麼都 不保證,所以第二次掃描是答案的一部分。
  • 還沒檢查兩個候選是否相符就先收養。 把 count1 == 0 放在 num == cand2 之前, [1,2,3,2] 會變成 cand1 == cand2 == 2:同一個值被追蹤兩次,回報成 [2, 2]。

練習 — LC 229 ⭐ · LC 169 (k = 2) · LC 2404 (最常出現的偶數:要計數,不是投票)

1-2-2) 頻率陣列+即時累計

當值有界(1..n 的排列、小寫字母)時,以值為索引的陣列就是雜湊表,而在頻率跨過門檻的那一刻 更新的計數器,可以取代任何重新掃描。

Template 4: Frequency Array + Running Count — LC 2657

辨識 — 「1..n 的排列」、「值在 [1, n] 內」,再加上「對每個前綴/每個索引,有幾個 …」。 LC 2657 的 n ≤ 50,但這個形狀是給 n ≤ 10⁵ → O(n) 用的,所以要避免的是每個前綴都重算一次(O(n²))。

關鍵 — count[v] 是目前為止看過 v 幾次,而 common 只在某個 count[v] 剛好達到門檻 (這裡是 2:A 裡一次、B 裡一次)的那一步改變。每個值最多跨過門檻一次,所以 common 正好等於 已經跨過的值的個數。

python
# python
# LC 2657 - Find the Prefix Common Array of Two Arrays
# IDEA: frequency array indexed by value; bump `common` the moment a count reaches 2
# time = O(n), space = O(n)
def findThePrefixCommonArray(A, B):
    n = len(A)
    count = [0] * (n + 1)        # values are 1..n, so index n must exist
    common = 0
    res = [0] * n
    for i in range(n):
        count[A[i]] += 1         # increment first ...
        if count[A[i]] == 2:     # ... then test, so the crossing step is the one counted
            common += 1
        count[B[i]] += 1
        if count[B[i]] == 2:
            common += 1
        res[i] = common
    return res
java
// java
// LC 2657 - Find the Prefix Common Array of Two Arrays
// IDEA: frequency array indexed by value; bump commonCount the moment a count reaches 2
// time = O(n), space = O(n)
public int[] findThePrefixCommonArray(int[] A, int[] B) {
    int n = A.length;
    int[] frequency = new int[n + 1];   // values are 1..n
    int commonCount = 0;
    int[] res = new int[n];
    for (int i = 0; i < n; i++) {
        frequency[A[i]]++;
        if (frequency[A[i]] == 2) {
            commonCount++;              // now seen in both arrays
        }
        frequency[B[i]]++;
        if (frequency[B[i]] == 2) {
            commonCount++;
        }
        res[i] = commonCount;
    }
    return res;
}

複雜度

  • 時間 O(n):一個 for i in range(n),每步兩次 O(1) 的陣列更新,不重掃前綴。
  • 空間 O(n):count = [0] * (n + 1),大小由值域決定(一般是 O(最大值))。

試跑

text
A = [1,3,2,4], B = [3,1,2,4]
i | A[i] B[i] | counts that reached 2 | common
0 |  1    3   | -                     | 0
1 |  3    1   | 3, 1                  | 2
2 |  2    2   | 2 (0 -> 1 -> 2 in one step) | 3
3 |  4    4   | 4                     | 4      -> [0, 2, 3, 4]

陷阱

  • 值是 1..n 卻開 [0] * n。 A = [1] 會在長度 1 的 list 上存取 count[1]:IndexError (或 ArrayIndexOutOfBoundsException)。要開 n + 1。
  • 先判斷再加一。 if count[x] == 2: common += 1 之後才 count[x] += 1,算到的是跨過門檻的 下一步,所以 A = [1], B = [1] 會回傳 [0] 而不是 [1]。
  • 陣列不是排列時用 >= 2 取代 == 2:出現三次的值會被算兩次。要判斷的是「跨過」,不是「狀態」。

不適用時 — 值無界或很稀疏(-10⁹ ≤ v ≤ 10⁹):開不了那麼大的陣列,改用雜湊表 (hash_map.md),做同樣的跨門檻判斷。

練習 — LC 2657 ⭐ · LC 217 (門檻 2,提早回傳) · LC 448 (計數 0:缺少的值) · LC 645 (一個值是 2、一個是 0) · LC 442 (同樣的事原地做,用正負號技巧)

1-2-3) 索引貢獻計數(統計每個元素在所有子陣列中出現幾次)

不要列舉每個子陣列,而是問「有幾個子陣列包含這個索引」,然後一次掃描把這些貢獻加總。

Template 5: Index Contribution Counting — LC 2063

辨識 — 「所有子字串/子陣列上 X 的總和」,而且每個元素各自對 X 有貢獻(母音算 1、值就加它 自己)。n ≤ 10⁵ → 子陣列約有 5·10⁹ 個,無法列出;預期 O(n)。

關鍵 — 包含索引 i 的子陣列,就是在 0..i 選左端、在 i..n-1 選右端,所以 i 恰好落在 (i + 1) * (n - i) 個子陣列裡。當元素的貢獻和它所在的子陣列無關時,總和就是 value(i) * (i + 1) * (n - i) 的加總。

text
# subarrays containing index i
   = (choices for LEFT boundary) × (choices for RIGHT boundary)
   = (i + 1) × (n - i)
     left  boundary ∈ {0, 1, ..., i}     → (i + 1) choices
     right boundary ∈ {i, i+1, ..., n-1} → (n - i) choices
python
# python
# LC 2063 - Vowels of All Substrings
# IDEA: each vowel at index i appears in (i+1)*(n-i) substrings; sum those products
# time = O(n), space = O(1)
def countVowels(word):
    vowels = set("aeiou")
    n = len(word)
    total = 0
    for i in range(n):
        if word[i] in vowels:
            starting_choices = i + 1      # left boundary: 0..i
            ending_choices = n - i        # right boundary: i..n-1
            total += starting_choices * ending_choices
    return total
java
// java
// LC 2063 - Vowels of All Substrings
// IDEA: each vowel at index i appears in (i+1)*(n-i) substrings; sum in a long
// time = O(n), space = O(1)
public long countVowels(String word) {
    long total = 0;
    int n = word.length();
    String vowels = "aeiou";
    for (int i = 0; i < n; i++) {
        if (vowels.indexOf(word.charAt(i)) >= 0) {
            long startingChoices = i + 1;   // left boundary: 0..i
            long endingChoices = n - i;     // right boundary: i..n-1
            total += startingChoices * endingChoices;
        }
    }
    return total;
}

複雜度

  • 時間 O(n):一個 for i in range(n);乘積取代了 O(n²) 的 (left, right) 列舉。
  • 空間 O(1):一個累計總和。

試跑

text
word = "aba" (n = 3)
i | char | vowel? | (i+1) × (n-i)
0 |  a   |  yes   | 1 × 3 = 3
1 |  b   |  no    | -
2 |  a   |  yes   | 3 × 1 = 3        total = 6
check: "a"=1 "ab"=1 "aba"=2 "b"=0 "ba"=1 "a"=1 -> 6

陷阱

  • 邊界差一。 i * (n - i) 漏掉了從 i 開始的子陣列;對 "a" 會回傳 0 而不是 1。左端有 i + 1 種選擇,右端有 n - i 種。
  • Java 的 int 溢位。 n = 10⁵ 時光是中間那個索引就貢獻約 2.5·10⁹,超過 Integer.MAX_VALUE。 用 long 累加,並在相乘之前就轉型。
  • 「包含 i」與「從 i 開始/在 i 結束」。 從 i 開始的子陣列:n - i;在 i 結束:i + 1; 包含 i:兩者相乘。把它們搞混是最常見的錯誤答案。

不適用時 — 貢獻取決於子陣列,例如元素只有在是最小值時才算(LC 907):它的範圍被最近的 較小元素限制住,所以跨度要用 monotonic stack 求,而不是 i + 1 和 n - i。

練習 — LC 2063 ⭐ · LC 1588 (奇數長度子陣列:數奇數長度的 (left, right) 配對) · LC 828 (唯一字元:到前一個與下一個相同字元的跨度) · LC 907 (最小值:單調堆疊跨度) · LC 2104 (最大值貢獻減最小值貢獻)

1-2-4) 反向寫入指標(原地合併/原地覆寫) 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

當輸出要寫進同一個還在讀取的陣列,而空位在尾端時,就從後面往前寫。

卡片在 2_pointers.md § Template 5: LC 88 Merge Sorted Array、讓它安全的不變量(write >= p1,所以正要寫入的格子不是已經讀過就是填充區)、 Python 和 Java、手動追蹤和陷阱。兩個讀取指標加一個寫入指標,讓它成為指標模式,所以由那份文件擁有; 這份文件只保留上面那條規則,因為那是關於覆寫陣列的事實。

合併以外的同一條規則:

  • LC 1089 Duplicate Zeros:先數有幾個零,就知道最後一個留下的值落在哪裡,再從後往前複製, 每個零寫兩次。
  • LC 977 Squares of a Sorted Array:最大的平方在兩端之一,所以從後面往前填結果。
  • LC 443 String Compression 則是相反的情況:輸出永遠不比已讀的部分長,所以正向寫入指標是安全的。

1-2-5) 陣列+雜湊索引表(用「和最後一個交換」做 O(1) 刪除) 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

從陣列中間刪除之所以是 O(n),只是因為要搬移。如果順序不重要,就用最後一個元素補洞,再彈出尾端。

Template 7: Swap-With-Last Delete — LC 380

辨識 — 「實作 insert、remove 和 getRandom,每個都要平均 O(1) 時間」、「每個元素被回傳 的機率必須相同」。最多 2·10⁵ 次呼叫 → 每個操作 O(1)。

關鍵 — vals 永遠是緊密的(沒有洞),所以 vals[隨機索引] 是均勻抽樣,而 idx[v] 永遠是 v 在 vals 裡的位置。刪除時把最後一個值移進洞裡、更新它的索引、再彈出尾端,就能同時維持這兩件事; 這也是 list 唯一 O(1) 的刪除方式。

text
remove(val):
1. i = idx[val]                  (hash map: value -> index)
2. vals[i] = vals[-1]            (plug the hole with the last value)
3. idx[vals[-1]] = i             (the moved value's index changed)
4. vals.pop(); del idx[val]      (O(1) tail removal)
python
# python
# LC 380 - Insert Delete GetRandom O(1)
# IDEA: list for O(1) random access + dict {value: index};
#       delete by swapping the victim with the LAST element, then pop
# time = O(1) per insert / remove / getRandom, space = O(n)
import random

class RandomizedSet(object):

    def __init__(self):
        self.vals = []   # dense array of values
        self.idx = {}    # value -> index in self.vals

    def insert(self, val):
        if val in self.idx:
            return False
        self.idx[val] = len(self.vals)
        self.vals.append(val)
        return True

    def remove(self, val):
        if val not in self.idx:
            return False
        i = self.idx[val]
        last = self.vals[-1]
        self.vals[i] = last          # plug the hole with the last element
        self.idx[last] = i
        self.vals.pop()
        del self.idx[val]            # after the re-index, so val == last still works
        return True

    def getRandom(self):
        return random.choice(self.vals)
java
// java
// LC 380 - Insert Delete GetRandom O(1)
// IDEA: ArrayList for O(1) random access + HashMap<value, index>;
//       delete by swapping the victim with the LAST element, then pop
// time = O(1) per insert / remove / getRandom, space = O(n)
class RandomizedSet {
    private final List<Integer> vals = new ArrayList<>();
    private final Map<Integer, Integer> idx = new HashMap<>(); // value -> index in vals
    private final Random rand = new Random();

    public boolean insert(int val) {
        if (idx.containsKey(val)) {
            return false;
        }
        idx.put(val, vals.size());   // the new value goes to the tail
        vals.add(val);
        return true;
    }

    public boolean remove(int val) {
        Integer i = idx.get(val);
        if (i == null) {
            return false;
        }
        int lastIdx = vals.size() - 1;
        int lastVal = vals.get(lastIdx);
        vals.set(i, lastVal);        // plug the hole with the last element
        idx.put(lastVal, i);
        vals.remove(lastIdx);        // remove by INDEX (int), not by object
        idx.remove(val);             // after the re-index, so val == lastVal still works
        return true;
    }

    public int getRandom() {
        return vals.get(rand.nextInt(vals.size()));
    }
}

複雜度

  • 時間:每次呼叫平均 O(1):每個方法只做常數次雜湊表操作,而且只碰 vals 的尾端(append、 pop);沒有任何搬移。
  • 空間 O(n):vals 和 idx 對每個存放的值各存一筆。

陷阱

  • 還沒重新指向就先刪掉 map 的項目。 當要刪的就是最後一個元素(insert(5); remove(5))時,先 del idx[val] 再 idx[last] = i 會把 5 放回 map,之後的 insert(5) 就回傳 False。
  • 從中間刪除。 vals.remove(val)/vals.remove(i) 會搬移後面的元素:每次刪除 O(n),正是交換 要避免的成本。
  • Java 的 remove 多載。 在 List<Integer> 上,用 int 呼叫 vals.remove(lastVal) 是依索引 刪除;要嘛刻意傳索引,要嘛用 Integer.valueOf(v) 依值刪除。
  • 用墓碑標記取代交換。 把格子標成已刪除會留下洞,getRandom 不是不均勻,就是得無上限地重試。

不適用時 — 允許重複(LC 381):一個值有好幾個位置,所以 idx 要把值對應到索引的集合, 交換時只更新被移動那個值的集合中的一個成員。

練習 — LC 380 ⭐ · LC 381 (有重複:值 -> 索引集合) · LC 710 (帶黑名單的隨機抽樣:把被列入 黑名單的低索引重新對應到尾端)

1-2-6) 有界候選列舉(可能的答案就那幾個)

有些「讓所有元素都相等」的題目看起來要搜尋每一個值,但限制條件把答案釘死在一兩個候選上。列舉 這些候選,每個用 O(n) 驗證。

Template 8: Bounded Candidate Enumeration — LC 1007

辨識 — 「讓一整列的每個元素/所有元素都等於同一個值,求最少操作數,否則 −1」,而每個元素 只有一兩種選擇。n ≤ 2·10⁴ → 每個候選 O(n) 沒問題,只要候選很少。

關鍵 — 如果某個值 x 能填滿整列,第 0 張骨牌的某一面一定已經是 x,所以 x ∈ {tops[0], bottoms[0]}。兩個候選、各用一次掃描驗證,就是全部的搜尋。

python
# python
# LC 1007 - Minimum Domino Rotations For Equal Row
# IDEA: the answer must be tops[0] or bottoms[0] -> verify both candidates
# time = O(n), space = O(1)
def minDominoRotations(tops, bottoms):
    n = len(tops)

    def rotations_to_show(x):
        # flips needed so one whole row shows x; -1 if some domino lacks x entirely
        rot_top = 0      # flips to put x on top
        rot_bottom = 0   # flips to put x on bottom
        for i in range(n):
            if tops[i] != x and bottoms[i] != x:
                return -1
            if tops[i] != x:
                rot_top += 1
            elif bottoms[i] != x:      # elif: x on both faces needs no flip at all
                rot_bottom += 1
        return min(rot_top, rot_bottom)

    res = rotations_to_show(tops[0])
    if res != -1 or tops[0] == bottoms[0]:   # one candidate only when the faces match
        return res
    return rotations_to_show(bottoms[0])
java
// java
// LC 1007 - Minimum Domino Rotations For Equal Row
// IDEA: the answer must be tops[0] or bottoms[0] -> verify both candidates
// time = O(n), space = O(1)
public int minDominoRotations(int[] tops, int[] bottoms) {
    int res = rotationsToShow(tops[0], tops, bottoms);
    if (res != -1 || tops[0] == bottoms[0]) {
        return res;
    }
    return rotationsToShow(bottoms[0], tops, bottoms);
}

// flips needed so one whole row shows x; -1 if some domino lacks x entirely
private int rotationsToShow(int x, int[] tops, int[] bottoms) {
    int rotTop = 0;
    int rotBottom = 0;
    for (int i = 0; i < tops.length; i++) {
        if (tops[i] != x && bottoms[i] != x) {
            return -1;
        }
        if (tops[i] != x) {
            rotTop++;
        } else if (bottoms[i] != x) {
            rotBottom++;
        }
    }
    return Math.min(rotTop, rotBottom);
}

複雜度

  • 時間 O(n):最多呼叫兩次 rotations_to_show,每次一個 for i in range(n) 掃描。
  • 空間 O(1):每個候選兩個計數器。

陷阱

  • 該用 elif 卻用了 else。 兩面都是 x 的骨牌不用翻。寫成 else: rot_bottom += 1 會把它算成 一次下排翻轉,所以 tops = [2,1]、bottoms = [2,2] 會回傳 1 而不是 0。
  • 第一個候選失敗就回傳,沒有試 bottoms[0]:tops = [1,2,3]、bottoms = [2,2,2] 對 1 失敗, 對 2 只需要翻 0 次。
  • 把 1..6 每個值都跑一遍。 LC 1007 會過,但真正重要的論證(「答案一定在第 0 張骨牌上」)才是 能遷移的那個,而且值域到 10⁹ 時也還成立。

練習 — LC 1007 ⭐ · LC 169 (投票留下一個候選;驗證它) · LC 2128 (第 0 列釘住其他每一列: 每列必須等於它或它的反相)

1-2-7) 其他高頻陣列題(速查)

題目 LC# Diff 一句話技巧
Verifying an Alien Dictionary 953 Easy 建 char -> rank 表,然後只比相鄰兩兩;短的若是前綴,就必須排在前面
Prison Cells After N Days 957 Medium 模擬到狀態重複為止,然後 N %= cycle_len — 把 N = 10^9 變成 O(cycle)
Longest Common Prefix 14 Easy 垂直掃描:固定第 j 欄,比所有字串的那個字元;第一次不匹配就停
Minimum Moves to Equal Array Elements 453 Medium 「對 n-1 個元素 +1」等價於「對一個元素 -1」⇒ 答案 = sum(nums) - n * min(nums)

1-2-8) 索引當雜湊(循環排序與正負號標記) 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

當值是 1..n,或只有這些值重要時,陣列自己的索引就是一張有 n 個桶的雜湊表:值 v 屬於索引 v - 1。 用法有兩種,都只要 O(1) 額外空間。把每個值搬到它的位置(循環排序,就是下面的模板),或是把 v - 1 那格變成負數來標記(正負號標記,在 array_examples.md 用 LC 448 和 LC 442 示範)。

Template 9: Cyclic Sort, Index as Hash — LC 41 ⭐⭐⭐⭐⭐

辨識 — 「值在 [1, n] 範圍內」、「找出缺少的/重複的/第一個缺少的正數」,並要求「O(n) 時間、 O(1) 額外空間」,這就排除了 set。n ≤ 10⁵。

關鍵 — 交換迴圈結束後,每個出現過的 1..n 值 v 都在索引 v - 1。每次交換都把一個值永久送回家, 已經在家的值不會再被搬動,所以第一個 nums[i] != i + 1 的索引 i,就指出第一個缺少的正數。

python
# python
# LC 41 - First Missing Positive
# IDEA: cyclic sort - swap each value v in 1..n to index v - 1; the first index that
#       holds the wrong value names the first missing positive
# time = O(n), space = O(1)
def firstMissingPositive(nums):
    n = len(nums)
    for i in range(n):
        # keep swapping until nums[i] is out of range or its home already holds it
        while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
            home = nums[i] - 1          # computed first: the swap below changes nums[i]
            nums[i], nums[home] = nums[home], nums[i]
    for i in range(n):
        if nums[i] != i + 1:
            return i + 1
    return n + 1                        # 1..n are all present
java
// java
// LC 41 - First Missing Positive
// IDEA: cyclic sort - swap each value v in 1..n to index v - 1, then scan for the first gap
// time = O(n), space = O(1)
public int firstMissingPositive(int[] nums) {
    int n = nums.length;
    for (int i = 0; i < n; i++) {
        // keep swapping until nums[i] is out of range or its home already holds it
        while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
            int home = nums[i] - 1;
            int tmp = nums[home];
            nums[home] = nums[i];
            nums[i] = tmp;
        }
    }
    for (int i = 0; i < n; i++) {
        if (nums[i] != i + 1) {
            return i + 1;
        }
    }
    return n + 1;          // 1..n are all present
}

複雜度

  • 時間 O(n):while 看起來是巢狀,但每次交換都把一個值永久送回家,所以所有 while 加起來最多 交換 n 次。最後的掃描是一次走訪。
  • 空間 O(1):交換都在 nums 裡進行;home 是唯一的額外變數。

試跑

text
nums = [3, 4, -1, 1], n = 4
i=0  3 -> home 2   [-1, 4, 3, 1]   -1 is out of range: stop
i=1  4 -> home 3   [-1, 1, 3, 4]
     1 -> home 0   [1, -1, 3, 4]   -1 is out of range: stop
i=2  3 is home
i=3  4 is home
scan: nums[1] = -1 != 2   -> answer 2

陷阱

  • 沒有判斷重複。 少了 nums[nums[i] - 1] != nums[i],[1, 1] 會把 1 和已經在家的 1 永遠交換下去。 要比的是值,不是 home != i。
  • Python 一行交換。 nums[i], nums[nums[i] - 1] = nums[nums[i] - 1], nums[i] 會先賦值 nums[i],於是第二個目標是用新的值算出來的。在 [3, 4, -1, 1] 上它會弄亂陣列而且永遠不停。 先把 home 算好。
  • 超出範圍的值(0、負數、> n)沒有位置。放著不管;最後的掃描會把它們的索引當成缺口回報。
  • 回傳 n:1..n 全部都在時,[1, 2] 必須回傳 3。

不適用時 — 不准修改輸入:LC 287 Find the Duplicate Number 禁止修改,所以它是在值域上二分搜尋, 或用 Floyd 的環偵測 (binary_search_on_answer.md)。 如果允許 O(n) 額外空間,用 set 更簡單,也不會動到輸入。

練習 — LC 41 ⭐ · LC 268 (缺少的數字:總和或 XOR 更簡單,循環排序也行) · LC 448 (每個缺少的值:用正負號標記) · LC 442 (每個重複的值:某格已經是負的) · LC 645 (集合不符:一個重複、一個缺少,同一次掃描找出)

1-2-9) 用三次反轉原地旋轉 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

旋轉 k 格就是讓陣列的兩個區塊互換位置,同時保持各自的內部順序。反轉可以原地做到這件事。

Template 10: Rotate by Three Reversals — LC 189 ⭐⭐⭐⭐

辨識 — 「把陣列往右旋轉 k 步」、「原地、O(1) 額外空間」;更一般地說,兩個相鄰的區塊要互換位置 並保持各自的內部順序(原地反轉字串裡的單字)。n ≤ 10⁵、k 最多 10⁵,所以 k 可能大於 n。

關鍵 — 令 A = nums[0 .. n-k-1]、B = nums[n-k .. n-1],往右旋轉就是把 A B 變成 B A。 把整個陣列反轉得到 rev(B) rev(A):每個區塊都已在最終位置,只是方向相反;再各自反轉一次就恢復內部順序。

python
# python
# LC 189 - Rotate Array
# IDEA: reverse all, then reverse the first k and the last n - k: AB -> rev(B)rev(A) -> BA
# time = O(n), space = O(1)
def rotate(nums, k):
    n = len(nums)
    k %= n                       # k may exceed n

    def reverse(lo, hi):
        while lo < hi:
            nums[lo], nums[hi] = nums[hi], nums[lo]
            lo += 1
            hi -= 1

    reverse(0, n - 1)            # rev(B) rev(A)
    reverse(0, k - 1)            # B rev(A)
    reverse(k, n - 1)            # B A
java
// java
// LC 189 - Rotate Array
// IDEA: reverse all, then reverse the first k and the last n - k
// time = O(n), space = O(1)
public void rotate(int[] nums, int k) {
    int n = nums.length;
    k %= n;                        // k may exceed n
    reverse(nums, 0, n - 1);       // rev(B) rev(A)
    reverse(nums, 0, k - 1);       // B rev(A)
    reverse(nums, k, n - 1);       // B A
}

private void reverse(int[] nums, int lo, int hi) {
    while (lo < hi) {
        int tmp = nums[lo];
        nums[lo] = nums[hi];
        nums[hi] = tmp;
        lo++;
        hi--;
    }
}

複雜度

  • 時間 O(n):三次 reverse 呼叫總共走過陣列兩遍,每一對做一次交換。
  • 空間 O(1):reverse 在 nums 裡面交換。

試跑

text
nums = [1, 2, 3, 4, 5, 6, 7], k = 3      A = [1,2,3,4], B = [5,6,7]
reverse all          [7, 6, 5, 4, 3, 2, 1]
reverse [0, k-1]     [5, 6, 7, 4, 3, 2, 1]
reverse [k, n-1]     [5, 6, 7, 1, 2, 3, 4]

陷阱

  • 沒有 k %= n。 [1, 2] 配 k = 5:reverse(0, 4) 會讀出界。
  • 先做兩段區塊反轉。 在整個陣列反轉之前先反轉 [0, k-1] 和 [k, n-1],會變成往左旋轉: [1..7]、k = 3 得到 [4, 5, 6, 7, 1, 2, 3]。
  • nums = nums[-k:] + nums[:-k] 只是重新綁定區域變數名稱,呼叫端的 list 沒變。 nums[:] = ... 可以修好,但會建一份 O(n) 的複本。
  • pop() 再 insert(0, …),做 k 次是 O(n·k):每次在開頭插入都要搬動整個 list。

不適用時 — 序列是鏈結串列(LC 61 Rotate List):找到新的尾巴重新接上,不需要反轉。 如果允許開新陣列,nums[i] 一次就能放到 (i + k) % n。

練習 — LC 189 ⭐ · LC 151 (反轉單字:整個字串反轉,再反轉每個單字) · LC 186 (LC 151 的原地版本) · LC 61 (旋轉鏈結串列:改成重新接線)

1-2-10) 一次掃描的即時狀態 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

很多陣列題根本不需要資料結構:掃一次,帶著決定答案的一兩個值走,例如某樣東西最後出現的索引、 目前為止的最大值,或是位置加方向。把這些值說出來,就是整道題。

Template 11: Running State Scan, Last-Seen Index — LC 849 ⭐⭐⭐⭐

辨識 — 「到最近的 … 的最大距離」、「前綴有幾次 …」、「會不會回到 …」:索引 i 的答案只取決於 它之前所有東西的一個摘要(最後一個有人坐的位子、目前最大值)。預期一次掃描、O(1) 額外空間;n ≤ 2·10⁴。

關鍵 — last 是最近一個有人坐的位子的索引。坐在 last 和 i 兩個有人的位子之間的空段, 可以得到距離 (i - last) // 2。第一個有人位子之前的那段、最後一個有人位子之後的那段,只有一邊有人, 所以整段長度都算。

python
# python
# LC 849 - Maximize Distance to Closest Person
# IDEA: carry the last occupied index; an inner gap scores (i - last) // 2,
#       the leading and the trailing gap score their full length
# time = O(n), space = O(1)
def maxDistToClosest(seats):
    n = len(seats)
    best = 0
    last = -1                      # no occupied seat seen yet
    for i in range(n):
        if seats[i] == 1:
            if last == -1:
                best = i           # leading gap: sit in seat 0
            else:
                best = max(best, (i - last) // 2)   # inner gap: sit in the middle
            last = i
    return max(best, n - 1 - last) # trailing gap: sit in the last seat (0 if it is taken)
java
// java
// LC 849 - Maximize Distance to Closest Person
// IDEA: carry the last occupied index; inner gaps are halved, the two end gaps are not
// time = O(n), space = O(1)
public int maxDistToClosest(int[] seats) {
    int n = seats.length;
    int best = 0;
    int last = -1;                 // no occupied seat seen yet
    for (int i = 0; i < n; i++) {
        if (seats[i] == 1) {
            if (last == -1) {
                best = i;          // leading gap: sit in seat 0
            } else {
                best = Math.max(best, (i - last) / 2);   // inner gap: sit in the middle
            }
            last = i;
        }
    }
    return Math.max(best, n - 1 - last);   // trailing gap
}

複雜度

  • 時間 O(n):對座位做一次 for,每一步 O(1)。
  • 空間 O(1):整個狀態就是 last 和 best。

試跑

text
seats = [0, 0, 1, 0, 1, 0, 0, 0]
i=2  first 1        leading gap  -> best = 2      last = 2
i=4  1              (4 - 2) // 2 = 1, best = 2    last = 4
end                 trailing gap 7 - 4 = 3        best = 3

陷阱

  • 把兩端的空段減半。 [0, 0, 0, 1] 會回傳 1 而不是 3:坐在第 0 號位子只有一邊有人。
  • 漏了尾端空段。 [1, 0, 0, 0] 會回傳 0 而不是 3,因為之後沒有任何 1 來結束最後那段。
  • Python 的 /。 (i - last) / 2 在 [1, 0, 0, 1] 上得到 1.5;要用 //。

不適用時 — 每個索引都要知道它到兩側最近標記的距離(LC 821 Shortest Distance to a Character): 兩次掃描,各從一端開始。如果帶著的值一旦被打敗就必須丟掉(下一個更大元素),狀態就是一個堆疊: monotonic_stack.md。

練習 — LC 849 ⭐ · LC 121 (目前為止的最低價:stock_trading.md) · LC 1375 (目前最大值等於步數) · LC 1041 (一次走完後的位置和方向) · LC 670 (從右邊掃,帶著最大值的索引)


2) 模式選擇

大多數被標成 array 的題目,其實不是陣列題。它們是視窗、指標、前綴和或排序的題目,只是剛好裝在陣列裡, 而那些各自都有自己的專頁。這張表的用途是讓你趕快離開這份文件——這裡只留真正屬於陣列本身的東西: 原地改寫它,以及把它的索引當成儲存空間。

題目真正在考的是… 去哪裡 為什麼不在這裡
一段長度或內容會變的連續區間 sliding_window.md 視窗的擴張/收縮規則才是整題的核心
兩個索引互相靠近,或一組快慢指標 2_pointers.md 不變量活在兩個指標之間,不在陣列裡
反覆查詢區間和或區間計數 prefix_sum.md 前處理本身就是技巧
大量區間更新、少量讀取 difference_array.md 你更新的是端點,不是整段區間
在已排序資料中找某個值或某個邊界 binary_search.md 陣列已排序是前提,不是招式
找和為目標值的兩數或三數 n_sum.md 排序加雙指標,整個家族一次講完
排序本身——自訂鍵、穩定性、部分排序 sort.md 比較器就是答案
帶平手規則的排序鍵,或混合方向的排序 python_trick.md 與 java_trick_strings_sorting.md 這是比較器的問題,不是陣列的問題
二維格子 matrix.md 走訪順序與邊界處理才是主戲
以每個索引結尾的最佳和/最佳乘積 kadane_algorithm.md 那是一行就寫完的 DP 遞迴式
帶限制的買進賣出 stock_trading.md 重點在那個狀態機

這份文件真正擁有的東西

如果你需要… 技巧 寫在哪
原地覆寫而不弄丟還沒讀的資料 反向寫入指標 — 從尾端(空位所在)開始填 1-2-4)
不用額外空間就記下「我看過值 v」 索引當雜湊,正負號標記 — 把 nums[v - 1] 變負號,讀取一律透過 abs() 1-2-8)、examples
把每個值放回它該在的索引 循環排序 — 一直交換直到 nums[i] == i + 1,再掃出缺口 1-2-8)
以 O(1) 刪除任意元素 跟最後一個交換再彈掉 — 外加一張值 → 索引的表 1-2-5)
用 O(1) 空間找多數元素 Boyer-Moore 投票 — 一個候選人加一個計數器,沒別的 1-2-1)
值域小又有界時統計出現次數 頻率陣列,不要用雜湊表 1-2-2)
原地旋轉 k 格 整個反轉、前 k 個反轉、剩下的反轉 1-2-9)
可能的結果就那幾種時該怎麼答 有界候選列舉 — 全部試一遍 1-2-6)
答案只取決於到目前為止的一個摘要 即時狀態 — 帶著最後的索引、目前最大值、位置 1-2-10)

三個陷阱

  1. 正向覆寫會毀掉還沒讀的輸入。 從前面開始把東西併進 nums1,會蓋掉還沒讀到的值。 要從後面往前填;尾巴才是保證空著的那一段。
  2. 用正負號做標記卻沒配 abs()。 一格被變成負的之後,直接拿它去讀就會得到負索引。 每次讀取都要經過 abs(...),而且如果呼叫端之後還要用這個陣列,記得把正負號還原。
  3. 索引當雜湊的前提是那些值本身要是合法索引。 在拿值當偏移量之前,先把 1..n 之外的東西夾住或跳過, 不然這招會變成陣列越界。

3) 題目詳解

題目詳解放在 array_examples.md,依各題歸約到的模板分類。它的 索引 也列出那些標成陣列、但歸屬在別份文件的題目。

模板 題目
Template 9: Cyclic Sort, Index as Hash LC 448, 442
Template 11: Running State Scan LC 1375, 1041, 670

4) 練習路徑

階段 題目(易 → 難) 增加了什麼
核心 LC 88 again · LC 169 ok · LC 238 ok ⭐ · LC 380 未評定 反向寫入(2_pointers);投票;前綴 × 後綴乘積(prefix_sum);和最後一個交換再刪除
練習 LC 849 未評定 · LC 448 未評定 · LC 189 ok · LC 229 未評定 · LC 251 ok · LC 1007 未評定 · LC 2063 未評定 · LC 2657 again 即時狀態;正負號標記;用三次反轉做旋轉;k − 1 個候選;惰性迭代器;從索引 0 取候選;貢獻計數;頻率陣列
延伸 LC 41 again · LC 381 未評定 · LC 828 未評定 循環排序/索引當雜湊;有重複時的交換刪除;以每個字元跨度計算的貢獻