陣列 — 實戰題解
範圍 — array.md 自己那些模板(索引當雜湊、一次掃描的即時狀態)背後的題目,每題都放在它歸約到的卡片底下;歸屬在別份文件的陣列標籤題,在這裡只留一列指標。 另見:array.md — 母表:基本操作、這些題目歸約到的模板,以及告訴你一題到底該歸哪張表的選擇表;2_pointers.md、sliding_window.md、prefix_sum.md、difference_array.md — 吃掉大部分 array 標籤題目的四個模式家族;sort.md、matrix.md、kadane_algorithm.md、stock_trading.md — 選擇表會把題目導向的其他主表。
LeetCode 題目清單
總覽
array.md 放模板;這份文件放應用它們的題目。每張卡片都寫出它歸約到哪個模板,以及多出來的那一步。 多數標成 array 的題目其實是視窗、指標、前綴和、排序或 DP 題:它們列在 其他歸屬,連到詳解它們的文件,所以每題都只在一個地方解。
索引
| 模式 | 題目(易 → 難) | 核心 ⭐ |
|---|---|---|
| Template 9: Cyclic Sort, Index as Hash | LC 448 · LC 442 | LC 41(錨定題,在 array.md) |
| Template 11: Running State Scan | LC 1375 · LC 1041 · LC 670 | LC 849(錨定題,在 array.md) |
其他歸屬
| LC | 題目 | 歸屬 | 為什麼放在那裡 |
|---|---|---|---|
| 41 | First Missing Positive | array § Template 9 | 循環排序卡片的錨定題 |
| 88 | Merge Sorted Array | 2_pointers § Template 5 | 反向寫入是兩個讀取指標加一個寫入指標 |
| 121 | Best Time to Buy and Sell Stock | stock_trading.md | 目前為止的最低價;股票家族共用一個狀態機 |
| 122 | Best Time to Buy and Sell Stock II | greedy § Template 3 | 貪心累加的錨定題:把每一段上漲加起來 |
| 189 | Rotate Array | array § Template 10 | 三次反轉卡片的錨定題 |
| 238 | Product of Array Except Self | prefix_sum.md | 前綴乘積乘上後綴乘積;輸出陣列兼當前綴陣列 |
| 251 | Flatten 2D Vector | array § Template 1 | 惰性雙游標迭代器的錨定題 |
| 287 | Find the Duplicate Number | binary_search_on_answer.md | 它禁止修改 nums,所以不能用正負號標記:在值域上二分搜尋,或用 Floyd 的環偵測 |
| 334 | Increasing Triplet Subsequence | patience_sorting.md | first / second 就是上限兩堆的 patience sorting 堆頂 |
| 406 | Queue Reconstruction by Height | sort.md | 先按身高由高到低排,再插到索引 k |
| 849 | Maximize Distance to Closest Person | array § Template 11 | 即時狀態卡片的錨定題 |
| 1109 | Corporate Flight Bookings | difference_array.md | 在端點上做區間更新,再做一次前綴和 |
| 1567 | Maximum Length of Subarray With Positive Product | kadane_algorithm.md | 以這裡結尾的最長正積/負積長度,Kadane 式 |
索引當雜湊題目
Find All Numbers Disappeared in an Array — LC 448
關鍵 — 把
nums[v - 1]變成負數,代表「看過值v」。掃完一次後,還是正數的格子就指出一個從沒出現過的值。 每次讀取都要經過abs(),因為它讀的那格可能已經被標記過。
歸約到:array § Template 9 — 用正負號標記而不是搬動值,並收集每個缺口而不只是第一個。
# python
# LC 448 - Find All Numbers Disappeared in an Array
# IDEA: sign marking - negate nums[v - 1] for every v; the indices left positive are missing
# time = O(n), space = O(1) besides the output
def findDisappearedNumbers(nums):
for x in nums:
i = abs(x) - 1 # x may already be negative: it was marked by an earlier value
if nums[i] > 0: # mark once; a second negation would unmark a duplicate
nums[i] = -nums[i]
missing = []
for i in range(len(nums)):
if nums[i] > 0:
missing.append(i + 1)
return missing
陷阱
- 沒有
> 0防護就取負。[1, 1]會把第 0 格翻兩次,又變回正數,回傳[1, 2]而不是[2]。 - 直接讀
x。 標記之後x可能是負的;nums[x - 1]在 Python 會默默從尾端索引,在 Java 會丟例外。 - 呼叫端的陣列被改了。 如果還要再用,用
abs還原。
相似題 — LC 268(只缺一個值:總和或 XOR 更簡單)· LC 645(一個重複、一個缺少,用同一組標記找出)
Find All Duplicates in an Array — LC 442
關鍵 — 第二次讀到值
v時,第v - 1格已經是負的:這次讀取就是重複。每個值最多出現兩次, 所以每次讀取做一次判斷,就能把每個重複值剛好回報一次。
歸約到:array § Template 9 — 正負號標記,發現某格已經是負的時,那就是答案,而不是缺口。
# python
# LC 442 - Find All Duplicates in an Array
# IDEA: sign marking - the first visit negates nums[v - 1], a second visit finds it negative
# time = O(n), space = O(1) besides the output
def findDuplicates(nums):
duplicates = []
for x in nums:
i = abs(x) - 1 # always through abs(): x itself may have been marked
if nums[i] < 0:
duplicates.append(i + 1) # second visit to value i + 1
else:
nums[i] = -nums[i] # first visit
return duplicates
陷阱
- 出現三次的值會被這個判斷回報兩次,但 LC 442 保證最多兩次;如果沒有這個保證,就另外記一個「已回報」標記。
- LC 287 看起來一樣,其實不是。 它禁止修改
nums,所以這個解法在那裡是錯的;見其他歸屬。
相似題 — LC 287(一個重複值,輸入唯讀)· LC 645(集合不符)
一次掃描的即時狀態題目
Number of Times Binary String Is Prefix-Aligned — LC 1375
關鍵 — 第
i步之後,位元1..i+1全部打開,剛好等價於目前翻過的最大索引是i + 1。 已經翻過i + 1個不同的索引(輸入是排列),而且沒有一個大於i + 1,所以它們正好就是1..i+1。
歸約到:array § Template 11 — 帶著的值是目前最大值。因為輸入是排列,它取代了 Template 4 的頻率陣列。
# python
# LC 1375 - Number of Times Binary String Is Prefix-Aligned (formerly Bulb Switcher III)
# IDEA: carry the largest index flipped so far; the prefix is aligned when it equals the step count
# time = O(n), space = O(1)
def numTimesAllBlue(flips):
largest = 0
aligned = 0
for step, index in enumerate(flips, start=1):
largest = max(largest, index)
if largest == step: # step indices flipped, none above step -> exactly 1..step
aligned += 1
return aligned
陷阱
- 拿從 0 開始的位置比。 用
enumerate(flips)的i寫largest == i,在[3, 2, 4, 1, 5]上永遠不成立, 回傳 0 而不是 2;索引是從 1 開始的。 - 每一步都檢查翻過的位元是 O(n²),在 n = 5·10⁴ 時會超時。
Robot Bounded In Circle — LC 1041
關鍵 — 把指令跑一次。回到原點的機器人是有界的。面向北方以外任何方向的機器人也有界: 跑四次(面向南方的話兩次)之後位移會互相抵消。只有結束時不在原點而且仍面向北方的機器人會越走越遠。
歸約到:array § Template 11 — 帶著的狀態是 (x, y, heading),答案從跑完一次後的狀態讀出。
# python
# LC 1041 - Robot Bounded In Circle
# IDEA: simulate one pass; bounded iff back at the origin or no longer facing north
# time = O(n), space = O(1)
def isRobotBounded(instructions):
moves = [(0, 1), (1, 0), (0, -1), (-1, 0)] # north, east, south, west: clockwise
x = 0
y = 0
heading = 0 # index into moves; 0 = north
for c in instructions:
if c == "L":
heading = (heading + 3) % 4 # one step counter-clockwise
elif c == "R":
heading = (heading + 1) % 4 # one step clockwise
else: # "G"
x += moves[heading][0]
y += moves[heading][1]
return (x == 0 and y == 0) or heading != 0
陷阱
- 只檢查原點。
"GL"結束在(0, 1)、面向西方,不在原點,但它走的是一個正方形,所以有界;方向的判斷會抓到它。 - Java 裡的
(heading - 1) % 4在heading為 0 時是-1,索引越界。像上面那樣改成加 3,而不是減 1。
Maximum Swap — LC 670
關鍵 — 從右往左掃數字,帶著目前看過的最大數字的索引(相同時取最右邊那個)。任何比這個最大值小的數字 都可以和它交換;最好的交換是最左邊的那個數字,而掃描是往左走的,所以最後記下的就是它。
歸約到:array § Template 11 — 從右邊掃,帶著每個數字右邊最大值的索引。
# python
# LC 670 - Maximum Swap
# IDEA: right-to-left scan carrying the index of the largest digit to the right;
# the leftmost digit smaller than that maximum is swapped with it
# time = O(d), space = O(d) d = number of digits (at most 9)
def maximumSwap(num):
digits = list(str(num))
max_idx = len(digits) - 1 # index of the largest digit in digits[i:]
left = -1 # the best swap found so far
right = -1
for i in range(len(digits) - 1, -1, -1):
if digits[i] > digits[max_idx]:
max_idx = i # strictly larger: ties keep the rightmost copy
elif digits[i] < digits[max_idx]:
left = i # further left than any earlier find, so better
right = max_idx
if left == -1:
return num # already the largest arrangement
digits[left], digits[right] = digits[right], digits[left]
return int("".join(digits))
陷阱
- 更新最大值時用
>=。1993必須和最右邊的 9 交換(9913);>=會把max_idx移到最左邊的 9,回傳 9193。 - 一找到候選就停。 在
98368上,從右邊第一個找到的是 6,得到 98386;最左邊比較小的數字是 3,得到 98863。 - 暴力試每一對是 O(d²) — 9 位數時沒問題,但那不是面試官想看的答案。
第二種做法 — 一張 {digit: last index} 表加上由左往右掃,在
hash_map_examples.md。同樣 O(d);它問的是「後面有沒有更大的數字?」,而不是帶著最大值走。