陣列
範圍 — 陣列的基本功——原地改寫、旋轉、分割,以及「用索引當雜湊」的招式。它擁有這些操作本身;至於各個模式家族(視窗、指標、前綴和)各有自己的檔案。 另見: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) 分類
-
相關主題
-
演算法
- 索引操作
- 陣列操作
- 排序
- binary search
- 2 pointers
- 快慢指標
- 左右指標
- sliding window
- prefix sum
- difference array
- Kadane algo
-
資料結構
- dict
- set
- array
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
# 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
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
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
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
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
# 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
# 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
// 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
# 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
# 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
# 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
// 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存的是參考而不是複本;狀態只有兩個整數。
試跑
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
# 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
# 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
// 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。
試跑
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
# 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
// 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
# 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
# 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
// 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(最大值))。
試跑
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) 的加總。
# 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
# 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
// 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):一個累計總和。
試跑
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) 的刪除方式。
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
# 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
// 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
# 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
// 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
# 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
// 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是唯一的額外變數。
試跑
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
# 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
// 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裡面交換。
試跑
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
# 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
// 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。
試跑
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) |
三個陷阱
- 正向覆寫會毀掉還沒讀的輸入。 從前面開始把東西併進
nums1,會蓋掉還沒讀到的值。 要從後面往前填;尾巴才是保證空著的那一段。 - 用正負號做標記卻沒配
abs()。 一格被變成負的之後,直接拿它去讀就會得到負索引。 每次讀取都要經過abs(...),而且如果呼叫端之後還要用這個陣列,記得把正負號還原。 - 索引當雜湊的前提是那些值本身要是合法索引。 在拿值當偏移量之前,先把
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 未評定 | 循環排序/索引當雜湊;有重複時的交換刪除;以每個字元跨度計算的貢獻 |