Binary Search
Scope — Halving a monotonic search space — the loop-invariant reasoning behind
l <= rvsl < r, the boundary (lower/upper bound) templates, rotated arrays, and floating-point and 2D search. See also — deep dives split out of this file: binary_search_on_answer.md — searching the answer space: thecanFinish/isValidpredicate, minimise-maximum vs maximise-minimum, and value-domain counting; binary_search_examples.md — the worked-problem archive, one canonical solution per problem. Neighbouring sheets: patience_sorting.md — §1.5’s scan told as the card game, with reconstruction, the pile/Dilworth proof and the LIS-reduction problems; sort.md — getting the array sorted first; advanced_divide_and_conquer.md — halving with a merge step; bst.md — the same invariant as a data structure; heap.md — k-th element without ordering; monotonic_stack.md — the positional “next greater”, which is the pattern lower bound is most often confused with.
LeetCode 題目清單
總覽
二分搜尋(Binary Search)是一個用雙指標在已排序的搜尋空間中尋找目標值的高效演算法。
關鍵性質
- 時間複雜度:O(log n)
- 空間複雜度:迭代 O(1),遞迴 O(log n)
- 前提條件:已排序的陣列,或具備單調性
- 搜尋空間:不限於完全排序的陣列,以下情況都適用:
- 完全排序的陣列
- 部分排序的陣列
- 旋轉排序陣列
- 任何具備單調性質的空間
核心演算法步驟
- 定義邊界:把
left與right指標初始化成涵蓋所有可能情況 - 定義回傳值:想清楚要回傳什麼(索引、值、-1 等)
- 定義結束條件:選對迴圈條件(
<=、<或< -1) - 更新指標:依與 target 的比較結果移動邊界
何時使用二分搜尋
- 已排序陣列:找確切值的經典場景
- 單調函數:只要
condition(k)能推得condition(k+1),就能用二分搜尋 - 搜尋邊界:找某個值第一次 / 最後一次出現的位置
- 最佳化問題:找滿足限制條件的最小 / 最大值
參考資料
- 框架:
- 題目集合:
- Python 工具:
- Python bisect module — 插入時維持排序順序
- Python Universal Binary Search Template — 一個模板通吃多題

理解二分搜尋的指標行為
核心洞見:l 和 r 到底代表什麼?
這是讓二分搜尋成立的根本概念,也解釋了為什麼像 LC 35 這種求插入位置的題目回傳 l 是對的。
迴圈進行中:搜尋空間不變式
在整個 while (l <= r) 迴圈期間:
l左邊的所有元素都嚴格< targetr右邊的所有元素都嚴格> targettarget可能出現的位置永遠落在[l, r]之內
每次迭代砍掉一半搜尋空間,同時保持這個不變式。
// Standard binary search pattern
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
l = mid + 1; // All elements [0..mid] are < target
} else {
r = mid - 1; // All elements [mid..end] are > target
}
}
迴圈結束時:指標的位置
迴圈在 l > r 時結束,也就是 l == r + 1。
在這一瞬間,陣列被切成兩部分:
Visual Representation:
index: 0 ... r l ... n-1
value: [< target] gap [> target]
迴圈結束時的關鍵性質:
r是最後一個小於 target 的元素l是第一個大於等於 target 的元素r和l之間不存在任何索引(因為r = l - 1)
這就是為什麼 l 正是正確的插入位置!
圖解範例
我們來追蹤 nums = [1, 3, 5, 6], target = 4:
Initial:
l=0, r=3
[1, 3, 5, 6]
l r
Step 1:
mid = 1, nums[1] = 3
3 < 4, so l = mid + 1 = 2
[1, 3, 5, 6]
l r
Step 2:
mid = 2, nums[2] = 5
5 > 4, so r = mid - 1 = 1
[1, 3, 5, 6]
r l
Loop ends (l > r):
- r points to 3 (last element < 4)
- l points to 5 (first element > 4)
- Insertion position is l = 2
總結表
| 狀態 | l 的位置 |
r 的位置 |
意義 |
|---|---|---|---|
| 迴圈進行中 | 第一個未檢查且 >= target 的索引 | 最後一個未檢查且 <= target 的索引 | 搜尋空間是 [l, r] |
| 迴圈結束 | 第一個 >= target 的元素 | 最後一個 < target 的元素 | l 就是插入點 |
| 圖示 | ... r | l ... |
兩者之間沒有空隙 | l = r + 1 |
應用:Search Insert Position (LC 35)
// LC 35 - The cleanest solution using pointer behavior
public int searchInsert(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return 0;
}
int l = 0;
int r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
// Key insight: l is always the correct insertion position
return l;
}
為什麼不需要特判就能成立:
- 若 target 存在:迴圈中就直接回傳 mid
- 若 target 不存在:
- 迴圈以
l > r結束 - 依不變式:
nums[0..l-1] < target且nums[l..end] >= target - 因此
l正是 target 該被插入的位置
- 迴圈以
該避開的常見錯誤
// ❌ WRONG: Trying to handle "between mid and mid+1" during the loop
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) return mid;
// This is unnecessary and error-prone!
// ❌ WRONG — can cause index out of bounds
if (mid + 1 <= nums.length - 1 && target < nums[mid+1] && target > nums[mid]) {
return mid + 1;
}
// ...
}
為什麼這樣寫是錯的:二分搜尋本來就會自然收斂到正確位置。相信指標不變式,迴圈結束後直接回傳 l 就好。
1) 二分搜尋的類型與模式
1.1) 類型速覽
基本二分搜尋 — LC 704(標準模板見 §2.1)
- 目的:在已排序陣列中找出確切的目標值
- 回傳:target 的索引,找不到則回傳 -1
- 複雜度:O(log n)
遞迴版二分搜尋
- 使用時機:遞迴寫法比較直觀時
- 空間:因呼叫堆疊而為 O(log n)
旋轉陣列搜尋(§1.2)
- 關鍵概念:判斷哪一半是有序的,再決定往哪邊搜尋
- 應用:找 target、找最小元素
找邊界 — 左 / 右,即 lower / upper bound(§1.3)
- 目的:找出第一個 / 最後一個滿足判定式的索引,而不是某個確切值
- 回傳:分界點 — 絕不
return mid
二維矩陣搜尋(§2.3)
- 做法 1:用
row = idx / cols、col = idx % cols把矩陣攤平 - 做法 2:逐列做二分搜尋
- 時間:O(log(m×n))
對答案空間做二分搜尋(§1.4)
- 目的:搜尋的是候選答案的範圍,而不是一個陣列
- 回傳:單調可行性判定式的分界點
對自己維護的陣列做二分搜尋 — tails 模式(§1.5)
- 目的:把「每個已達成長度的最佳值」這種
O(n²)DP 壓成一次 lower bound 查詢 - 回傳:所維護陣列的長度(是長度,不是那條鏈本身)
1.2) 旋轉排序陣列 — 找出樞紐點
- 關鍵概念:判斷哪一半是有序的,再決定往哪邊搜尋
- 應用:找 target、找最小元素
Find Minimum in Rotated Sorted Array (LC 153) Priority 5 of 5 — Must know — expect it in almost every loop
模式:找出旋轉點
旋轉排序陣列一定長這個樣子:
[Left Higher Plateau] > [Right Lower Plateau]
e.g. [3, 4, 5, 1, 2]
^^^^^^^^ ^^^^
left part right part (contains minimum)
最小值永遠在旋轉點上 — 也就是陣列中唯一那個「下墜」的位置。
核心想法
判斷 mid 落在哪一段平台上,然後往沒有排序的那一側移動(最小值就在那裡):
Rotation examples (length 5):
[1, 2, 3, 4, 5] → already sorted, min at index 0
[5, 1, 2, 3, 4] → mid < r → right is sorted → go left (r = mid)
[4, 5, 1, 2, 3] → mid < r → right is sorted → go left (r = mid)
[3, 4, 5, 1, 2] → mid >= l → left is sorted → go right (l = mid + 1)
[2, 3, 4, 5, 1] → mid >= l → left is sorted → go right (l = mid + 1)
判斷規則:
nums[mid] >= nums[l]→ mid 在左平台上 → 最小值在右邊 →l = mid + 1nums[mid] < nums[l]→ mid 在右平台上 → 最小值就是 mid 或在其左邊 →r = mid - 1
推薦模板(閉區間邊界,追蹤 ans)
// LC 153 - Find Minimum in Rotated Sorted Array
// time = O(log N), space = O(1)
public int findMin(int[] nums) {
int l = 0, r = nums.length - 1;
int ans = nums[0];
while (l <= r) {
// Early exit: current window already sorted → minimum is at l
if (nums[l] <= nums[r]) {
ans = Math.min(ans, nums[l]);
break;
}
int mid = l + (r - l) / 2;
ans = Math.min(ans, nums[mid]);
if (nums[mid] >= nums[l]) {
l = mid + 1; // left plateau → go right
} else {
r = mid - 1; // right plateau → go left
}
}
return ans;
}
替代模板(開區間邊界 r > l,不用 ans 變數)
// Cleaner: converges l == r to the minimum index
// time = O(log N), space = O(1)
public int findMin(int[] nums) {
int l = 0, r = nums.length - 1;
while (r > l) {
int mid = l + (r - l) / 2;
if (nums[mid] < nums[r]) {
r = mid; // right side sorted → min could be at mid
} else {
l = mid + 1; // left side sorted → min is to the right
}
}
return nums[l]; // l == r → minimum
}
視覺追蹤:nums = [3,4,5,1,2]
l=0, r=4: nums[l]=3 > nums[r]=2 → rotated
mid=2, nums[2]=5 >= nums[0]=3 → left plateau → l=3
l=3, r=4: nums[l]=1 < nums[r]=2 → sorted → ans=min(ans,1), break
Answer = 1 ✓
模板比較
| 模板 | 迴圈條件 | 更新方式 | 回傳 | 適用時機 |
|---|---|---|---|---|
閉區間 l <= r + ans |
l <= r |
l=mid+1 / r=mid-1 |
ans |
需要記錄候選答案時 |
開區間 r > l |
r > l |
r=mid / l=mid+1 |
nums[l] |
最乾淨,會收斂到索引 |
相似題目
| LC # | 題目 | 關鍵差異 |
|---|---|---|
| 153 | Find Minimum in Rotated Sorted Array | 元素不重複,找最小值 |
| 154 | Find Minimum in Rotated Sorted Array II | 有重複值 — nums[mid]==nums[r] 時用 r-- |
| 33 | Search in Rotated Sorted Array | 找 target(不是最小值)— 要檢查 target 是否落在有序的那一半 |
| 81 | Search in Rotated Sorted Array II | 在有重複值的情況下找 target |
| 189 | Rotate Array | 概念相關,但任務不同 |
Search in Rotated Sorted Array (LC 33, LC 81)
# LC 033. Search in Rotated Sorted Array
# LC 081. Search in Rotated Sorted Array II
# V0
# IDEA : BINARY SEARCH
# -> CHECK WHICH PART IS ORDERING
# -> CHECK IF TARGET IS IN WHICH PART
# CASES :
# 1) if mid is on the right of pivot -> array[mid:] is ordering
# -> check if mid in on the left or right on mid
# -> binary search on left or right sub array
# 2) if mid in on the left of pivot -> array[:mid] is ordering
# -> check if mid in on the left or right on mid
# -> binary search on left or right sub array
### NOTE : THE NESTED IF ELSE CONDITION
class Solution(object):
def search(self, nums, target):
if not nums: return -1
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
#---------------------------------------------
# Case 1 : nums[mid:right] is ordering
#---------------------------------------------
# all we need to do is : 1) check if target is within mid - right, and move the left or right pointer
if nums[mid] < nums[right]:
# mind NOT use (" nums[mid] < target <= nums[right]")
# mind the "<="
if target > nums[mid] and target <= nums[right]: # check the relationship with target, which is different from the default binary search
left = mid + 1
else:
right = mid - 1
#---------------------------------------------
# Case 2 : nums[left:mid] is ordering
#---------------------------------------------
# all we need to do is : 1) check if target is within left - mid, and move the left or right pointer
else:
# # mind NOT use (" nums[left] <= target < nums[mid]")
# mind the "<="
if target < nums[mid] and target >= nums[left]: # check the relationship with target, which is different from the default binary search
right = mid - 1
else:
left = mid + 1
return -1
// LC 33 - Search in Rotated Sorted Array
// IDEA: Binary search — identify sorted half, narrow range
// time = O(log N), space = O(1)
public int search(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == target) return mid;
if (nums[l] <= nums[mid]) { // left half is sorted
if (nums[l] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
} else { // right half is sorted
if (nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid - 1;
}
}
return -1;
}
關鍵差異:
- LC 153(找最小值):只需要判斷該往哪一側搜尋
- LC 33/81(找 target):還必須檢查 target 是否落在有序的那一半裡
1.3) Find Boundaries — Lower and Upper Bound (LC 34) Priority 5 of 5 — Must know — expect it in almost every loop
Purpose: Answer bound queries on a sorted array — the first index >= target, the
last index <= target, and everything that reduces to them (first/last occurrence, insertion
point, floor/ceiling lookups)
Recognition — “the smallest value >= target” is a Lower Bound Priority 5 of 5 — Must know — expect it in almost every loop
Before choosing a template, read the problem’s own wording. A query phrased as 「最小的 value >= target」 — the smallest value that is at least X — is a lower bound, no matter how the problem dresses it up (intervals, timestamps, spell strengths, LIS tails).
Pattern: Sort Once, Then One Bound Query per Element
| Wording in the problem | What you are asking for | Template |
|---|---|---|
| “smallest value >= X”, “first one that is at least X” | lower bound | findLeft → l (bisect_left) |
| “smallest value > X”, “strictly greater” | upper bound | findRight → r + 1 (bisect_right) |
| “largest value <= X”, “floor”, “most recent before X” | upper bound − 1 | findRight → r (bisect_right - 1) |
| “largest value < X” | lower bound − 1 | findLeft → l - 1 (bisect_left - 1) |
The shape is always the same three lines — and when the answer must be the element’s original position, pair the value with its index before sorting so the sort keeps them glued together:
# python - the generic "sort once, lower-bound each query" shape
# time = O(n log n) build + O(log n) per query, space = O(n)
import bisect
pairs = sorted((v, i) for i, v in enumerate(raw)) # NOTE !!! pair value WITH original idx
keys = [v for v, _ in pairs] # bisect needs a plain sorted list
j = bisect.bisect_left(keys, x) # first value >= x
ans = pairs[j][1] if j < len(keys) else -1 # map sorted pos -> original idx
// java - the same query, hand-rolled (identical to findLeft in the template below)
// time = O(log N), space = O(1)
private int lowerBound(int[] keys, int x) {
int l = 0, r = keys.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (keys[mid] < x) l = mid + 1; // strict < → equality falls right, pushing l left
else r = mid - 1;
}
return l; // l == keys.length → no value >= x
}
Core Idea: Lower Bound vs Monotonic Stack
These two get confused constantly, because both sound like “find the next bigger thing”. They answer different questions:
Monotonic stack : for each i, the FIRST element to its LEFT / RIGHT that is > (or <) nums[i]
-> a POSITIONAL neighbour; array order is the whole point
Lower bound : for each query x, the SMALLEST value >= x in the WHOLE set
-> a VALUE ranking; position is irrelevant, so sort first
Test to apply: if reordering the input would change the answer, it is a monotonic stack (a nearest-neighbour scan). If it would not — you would still want the same minimal value — it is a lower bound, so sort once and binary search.
LC 436 (Find Right Interval) is the clean example of the second: it wants the interval
whose start is the minimum start >= end_i across all intervals, not the nearest
interval sitting to the right in the input array — so it is binary search, not a
monotonic stack.
current.end
↓
all intervals' starts (sorted)
↓
first start >= end → lower bound
Worked solution for LC 436 — including the sort-with-index recipe — lives in binary_search_examples.md §16.
Similar Problems: Bound Queries on a Sorted Set
| LC # | Problem | The query, in bound form |
|---|---|---|
| 436 | Find Right Interval | smallest start >= end_i → lower bound, answer is the original index |
| 35 | Search Insert Position | smallest index with nums[i] >= target → lower bound, unvalidated |
| 34 | Find First and Last Position of Element in Sorted Array | lower bound and upper bound − 1 together |
| 744 | Find Smallest Letter Greater Than Target | strictly > → upper bound, then wrap with % n |
| 981 | Time Based Key-Value Store | largest timestamp <= query → upper bound − 1, per key |
| 1146 | Snapshot Array | same floor query, on a per-index version list |
| 300 | Longest Increasing Subsequence (O(N log N)) | smallest tail >= x, then overwrite it — §1.5 |
| 2300 | Successful Pairs of Spells and Potions | smallest potion >= ceil(success / spell), then count the suffix |
| 1170 | Compare Strings by Frequency of the Smallest Character | count of words with freq > query → n - upperBound |
If the set mutates between queries (insertions arriving over time), a sorted array plus binary search is no longer enough — reach for
SortedList/ a BST / a BIT instead.
模式:兩次獨立的邊界搜尋
有重複值的排序陣列可以看成三個區塊:
nums = [5, 7, 7, 8, 8, 10], target = 8
[ < target ] [ == target ] [ > target ]
5 7 7 8 8 10
0 1 2 3 4 5
^ ^
first last
因為相等的那個區塊是連續的,一次「找確切值」的二分搜尋毫無用處(它會停在區塊內的任意位置)。正確做法是跑兩次獨立的搜尋,各自去找一個區塊邊緣:
findLeft→ 第一個滿足nums[i] >= target的索引(相等區塊的起點)findRight→ 最後一個滿足nums[i] <= target的索引(相等區塊的終點)
核心想法 ⭐⭐⭐⭐⭐
關鍵想法:不要去找那個值 — 去找「太小」與「夠大」之間的分界點。絕不提早 return mid;持續縮小範圍,把指標擠到邊緣上。
這兩個輔助函式是只差一個字元的同一段程式碼(< vs <=),而且它們完全不做相等判斷:
| 輔助函式 | 移動 l 的條件 |
相等時走哪條路 | 回傳 |
|---|---|---|---|
findLeft |
nums[mid] < target |
走進 else → r = mid - 1(往左推) |
l |
findRight |
nums[mid] <= target |
走進 if → l = mid + 1(往右推) |
r |
為什麼這樣成立 — 結束時 l 與 r 已經交錯成 r == l - 1,正好夾住分界點:
after findLeft : everything left of l is < target → l = first index >= target
after findRight: everything right of r is > target → r = last index <= target
為什麼 [l, r] 同時也是有效性檢查(不必再回頭讀 nums):
target present → l = first equal idx, r = last equal idx → l <= r ✅
target absent → both searches collapse to the same gap:
l = insertion point p, r = p - 1 → l > r ❌ return [-1,-1]
與 Python bisect 的對應關係 — 把這個對照記起來,兩個輔助函式就再也忘不掉:
findLeft(nums, target) == bisect.bisect_left(nums, target) # count of elements < target
findRight(nums, target) == bisect.bisect_right(nums, target) - 1 # count of elements <= target, minus 1
推薦模板(兩個輔助函式,閉區間 l <= r)
# python - LC 34 Find First and Last Position of Element in Sorted Array
# IDEA: two binary searches — left boundary + right boundary
# time = O(log N), space = O(1)
class Solution:
def searchRange(self, nums, target):
l = self.findLeft(nums, target)
r = self.findRight(nums, target)
# NOTE !!! l <= r is the existence check (no nums[] lookup needed)
return [l, r] if l <= r else [-1, -1]
def findLeft(self, nums, target):
l, r = 0, len(nums) - 1
while l <= r:
mid = l + (r - l) // 2
# NOTE !!! strict `<` → equality falls to else → keep pushing LEFT
if nums[mid] < target:
l = mid + 1
else:
r = mid - 1
return l # NOTE !!! return l
def findRight(self, nums, target):
l, r = 0, len(nums) - 1
while l <= r:
mid = l + (r - l) // 2
# NOTE !!! `<=` → equality goes into if → keep pushing RIGHT
if nums[mid] <= target:
l = mid + 1
else:
r = mid - 1
return r # NOTE !!! return r
// java - LC 34 Find First and Last Position of Element in Sorted Array
// IDEA: two binary searches — left boundary + right boundary
// time = O(log N), space = O(1)
public int[] searchRange(int[] nums, int target) {
int l = findLeft(nums, target);
int r = findRight(nums, target);
return l <= r ? new int[]{l, r} : new int[]{-1, -1};
}
private int findLeft(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] < target) l = mid + 1; // strict <
else r = mid - 1;
}
return l;
}
private int findRight(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] <= target) l = mid + 1; // <= (the only difference)
else r = mid - 1;
}
return r;
}
替代解法 1:一個輔助函式,呼叫兩次(target 與 target + 1)⭐⭐⭐⭐⭐
最乾淨的技巧 — 只寫 bisect_left,然後注意到 target 最後一次出現的位置
就在 target + 1 第一次出現的位置的前一格:
# python - LC 34, half-open boundary [lo, hi)
# time = O(log N), space = O(1)
class Solution:
def searchRange(self, nums, target):
def search(x):
"""first index i where nums[i] >= x (== bisect_left)"""
lo, hi = 0, len(nums) # NOTE: hi = len(nums), NOT len-1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] < x:
lo = mid + 1
else:
hi = mid # NOTE: no -1 (half-open)
return lo
lo = search(target)
hi = search(target + 1) - 1 # last idx of target = first idx of target+1, minus 1
return [lo, hi] if lo <= hi else [-1, -1]
替代解法 2:相等時記錄 bound(最明確)
保留 nums[mid] == target 這個分支,並記下目前為止看到的最佳候選:
# python - LC 34, explicit "record then keep searching"
# time = O(log N), space = O(1)
class Solution:
def searchRange(self, nums, target):
def find_bound(is_first):
l, r = 0, len(nums) - 1
bound = -1
while l <= r:
mid = l + (r - l) // 2
if nums[mid] == target:
bound = mid # record candidate
if is_first:
r = mid - 1 # DON'T return — keep going left
else:
l = mid + 1 # keep going right
elif nums[mid] < target:
l = mid + 1
else:
r = mid - 1
return bound
left = find_bound(True)
if left == -1: # early exit: target absent
return [-1, -1]
return [left, find_bound(False)]
替代解法 3:bisect 一行解(面試的保底寫法 / 驗算用)
# python - LC 34 via bisect
# time = O(log N), space = O(1)
import bisect
class Solution:
def searchRange(self, nums, target):
left = bisect.bisect_left(nums, target)
if left >= len(nums) or nums[left] != target:
return [-1, -1]
return [left, bisect.bisect_right(nums, target) - 1]
視覺追蹤:nums = [5,7,7,8,8,10]、target = 8
findLeft (strict <, return l):
l=0 r=5 mid=2 nums[2]=7 < 8 → l=3
l=3 r=5 mid=4 nums[4]=8 !< 8 → r=3
l=3 r=3 mid=3 nums[3]=8 !< 8 → r=2
l=3 > r=2 → exit, return l=3 ✓ first index of 8
findRight (<=, return r):
l=0 r=5 mid=2 nums[2]=7 <= 8 → l=3
l=3 r=5 mid=4 nums[4]=8 <= 8 → l=5
l=5 r=5 mid=5 nums[5]=10 > 8 → r=4
l=5 > r=4 → exit, return r=4 ✓ last index of 8
l=3 <= r=4 → [3, 4] ✓
target 不存在的情況,nums = [5,7,7,8,8,10]、target = 6:
findLeft → l = 1 (insertion point)
findRight → r = 0 (= insertion point - 1)
l=1 > r=0 → [-1, -1] ✓
模板比較
| 模板 | 邊界 | 迴圈 | 「太小」時的更新 | 回傳 | 適用時機 |
|---|---|---|---|---|---|
兩個輔助函式 < / <= |
閉區間 [l, r] |
l <= r |
l = mid + 1 / r = mid - 1 |
l / r |
推薦 — 對稱、沒有相等分支、l <= r 順便驗證 |
一個輔助函式,target 與 target+1 |
半開區間 [lo, hi) |
lo < hi |
lo = mid + 1 / hi = mid |
lo |
要背的程式碼最少;只適用於整數 target |
相等時追蹤 bound |
閉區間 [l, r] |
l <= r |
保留三向 if/elif/else |
bound |
口頭講解時最好懂 |
bisect_left / bisect_right |
— | — | — | — | 允許用函式庫的 Python 面試 |
常見陷阱
- ❌
nums[mid] == target時就return mid→ 停在區塊中間,而不是邊緣 - ❌ 從
findLeft回傳r(或從findRight回傳l)→ 差一錯誤;左搜尋回傳l,右搜尋回傳r - ❌ 混用邊界風格:
hi = len(nums)必須搭配while lo < hi與hi = mid(不減 1);r = len(nums)-1必須搭配while l <= r與r = mid - 1 - ❌ 忘了空陣列的情況 — 兩個閉區間輔助函式都會自然處理(
l=0, r=-1→ 跳過迴圈 →l=0 > r=-1→[-1,-1]) - ❌ 事先檢查
if target not in nums— 那是 O(N),直接毀掉 O(log N) 的要求
相似題目
| LC # | 題目 | 關鍵差異 |
|---|---|---|
| 34 | Find First and Last Position of Element in Sorted Array | 基準題 — 兩個邊界都要 |
| 35 | Search Insert Position | 只用 findLeft,回傳 l 且不做驗證 |
| 704 | Binary Search | 只找確切值,不需處理重複 |
| 278 | First Bad Version | 對布林判定式(而非 <)做 findLeft |
| 852 / 162 | Peak Index / Find Peak Element | 以 nums[mid] < nums[mid+1] 為邊界條件 |
| 744 | Find Smallest Letter Greater Than Target | bisect_right + 取模繞回 |
| 1146 | Snapshot Array | 對每個索引的版本清單做 bisect |
| 658 | Find K Closest Elements | 用 findLeft 定位視窗起點,再往外擴 |
| 300 | Longest Increasing Subsequence (O(N log N)) | 用 bisect_left 替換 tails — §1.5 |
| 981 | Time Based Key-Value Store | findRight(最大且 <= 查詢值的時間戳) |
| 436 | Find Right Interval | 對排序後的起點做 findLeft |
| 1898 | Maximum Number of Removable Characters | 對答案做邊界搜尋 + 可行性檢查 |
1.4) 對答案空間做二分搜尋 Priority 5 of 5 — Must know — expect it in almost every loop
不是在陣列裡面找某個值,而是對候選答案的範圍做二分搜尋,並用一個可行性判定式決定要留下哪一半。它是 tier-5 的二分搜尋技巧中最少被練到的一項,因此獨立成一份文件:
完整內容 —
canFinish/isValid的思考框架、最小化 vs 最大化的 決策矩陣、left = max(nums)/right = sum(nums)的邊界配方、 單調判定式的證明、值域計數,以及所有題解 (LC 875、410、1011、1283、1482、1231、2616、1631、378、287、1539): binary_search_on_answer.md。
辨識關鍵字:「minimize the maximum」、「maximize the minimum」、「找出最小的 capacity / speed / divisor」、「能不能切分 / 分配 / 派送」。
1.5) 對自己維護的陣列做二分搜尋 — tails 模式 (LC 300) Priority 5 of 5 — Must know — expect it in almost every loop
上面每個模板搜尋的都是輸入陣列。這一個搜尋的是掃描過程中一邊走一邊建出來的小陣列 —
而那個陣列是刻意保持有序的,正是為了能對它做 lower bound。這就是 O(n log n) LIS 背後的模式,
也是 LC 300 會被歸在二分搜尋底下的原因。
這個演算法自己的專屬 sheet — 它源自的紙牌遊戲、牌堆 / Dilworth 論證、如何還原出子序列本身, 以及可以歸約成 LIS 的題目 — 在 patience_sorting.md。本節是二分搜尋 的視角:跑的是哪一個模板,以及為什麼「每個元素只寫一次」就是完整的更新。
核心想法 — 每個可達成的長度一格,存最小的結尾值
# python - LC 300 Longest Increasing Subsequence, O(n log n)
# IDEA: tails[k] = the SMALLEST tail value among increasing subsequences of length k+1
# -> tails is sorted -> lower-bound it (findLeft, §1.3) -> append or overwrite
# time = O(n log n), space = O(n)
class Solution(object):
def lengthOfLIS(self, nums):
# tails[k] : smallest possible ending value of an increasing run of length k + 1
tails = []
for num in nums:
# NOTE !!! this IS findLeft from §1.3, run on `tails` instead of on `nums`
l, r = 0, len(tails) - 1
while l <= r:
mid = l + (r - l) // 2
if tails[mid] < num:
l = mid + 1 # strict < -> equality fails this test, so it
# takes the else and pushes r left
else:
r = mid - 1
# l == lower_bound(tails, num) == first index with tails[l] >= num
if l == len(tails):
tails.append(num) # num beats every tail -> a NEW longest length exists
else:
tails[l] = num # same length, cheaper tail -> overwrite
return len(tails) # NOTE !!! the LENGTH is the answer, not the contents
bisect_left一行版、Java 版,以及 LC 354 的變化題都在 binary_search_examples.md §18。這裡刻意把迴圈完整寫出來: 它和findLeft(§1.3)逐字相同,而這正是這題該放在本 sheet 的理由。
為什麼可行 — 三個論證
1) tails 永遠有序,所以二分搜尋是合法的。
先假設它嚴格遞增,再檢查掃描可能走的兩種情況。只要格子 l-1 存在,lower bound 就保證
tails[l-1] < num;只要格子 l 存在,就保證 num <= tails[l]:
- append(
l == len(tails)):陣列尾端多出num,且tails[l-1] < num— 仍然嚴格遞增。 - overwrite(
l < len(tails)):num取代tails[l],新的鄰居關係是tails[l-1] < num <= tails[l] < tails[l+1]— 最後一項只在格子l+1存在時才需要看, 而且換成更小的值在哪種情況下都不會破壞順序。
所以處理完每個元素後陣列都仍嚴格遞增,這讓判定式 tails[i] < num 在前綴為真、在後綴為假 —
也就是任何二分搜尋都需要的單調條件。
2) 每次最多只有一個格子會被改善,而那個格子就是 l。
num 只有在某個長度 k+1 的遞增序列結尾 < num 時,才能接在它後面。因為 tails 有序,
結尾 < num 的長度剛好是 k = 0 .. l-1,所以 num 能延伸的最長序列長度是 l,接出來的
新長度是 l+1:
tails: [ tails[0] ... tails[l-1] | tails[l] ... ]
< num (extendable) >= num
^ longest extendable run has length l
-> num is a candidate tail for length l + 1 -> slot l
- 格子
l會被改善:新的候選結尾是num,而num <= tails[l],所以min(tails[l], num) = num。(若l已超出尾端,代表長度l+1之前做不到、現在做到了 — 那就是append。) - 任何
j < l的格子不會被改善:num可以 當長度j+1的結尾,但tails[j] < num, 已經存著更小的結尾。 - 任何
j > l的格子不會被改善:要透過num達到長度j+1,需要一個長度j且結尾< num的序列,但tails[j-1] >= tails[l] >= num,這樣的序列並不存在。
所以「每個元素只寫一次」不是最佳化 — 它就是完整的更新。
3) len(tails) 就是 LIS 長度。
>=:存在的每個格子都是某次 append 產生的,而 append 只在 num 真的延伸了某個序列時才發生 —
所以長度 len(tails) 的遞增序列確實存在。
<=:一個長度 L 的遞增子序列讓長度 L 變成可達成,依論證 2,tails 一定會有格子 L-1。
兩個方向都無法再更好。
tails 不是一個子序列。 只有它的長度有意義:
nums = [3, 4, 5, 1]
tails = [1, 4, 5] <- 1 sits at index 0 but arrives LAST in the input
len = 3 <- correct: the LIS is [3,4,5]
overwrite 所編碼的那個貪心
tails[l] = num 就是「結尾更小絕不會更糟」這個貪心:任何未來能接在舊結尾後面的 y,也一定能
接在 num 後面,甚至更多。這個覆寫不會丟掉任何可達成的答案 — 而這正是論證 2 所證明的事。它和
patience sorting 是同一個動作:把每張牌放到「牌頂 >= num 的最左邊那一堆」,沒有這樣的牌堆就
開一堆新的,答案就是牌堆數。
模式:怎麼認出它
當以下全部成立時,就該想到 tails:
- 答案是最長鏈的長度(或
n − 長度),不是鏈本身,也不是這種鏈有幾條; - 「可以接起來」是單一 key 上的全序(數值上的
<,或某個可以先排序的 key),所以中途的 進度可以用一個結尾值總結; - 顯然的解法是
O(n²)DPdp[i] = max(dp[j]) + 1,而追問要求O(n log n)。
這個推廣值得記住:當一個 DP 的狀態是「每個已達成長度的最佳值」而那張表是單調的,這張表就可以 二分搜尋,整列 DP 轉移也就塌成一次 lower bound 查詢。
| 差一位的地方 | 查詢 | Python |
|---|---|---|
| 嚴格遞增 (LC 300) | 第一個 >= num 的結尾 |
bisect_left |
| 非遞減(允許重複) | 第一個 > num 的結尾 |
bisect_right |
| 非遞增 / 遞減 | 把數值取負,再用上面的做法 | 對 -num 用 bisect_* |
要的是子序列本身,不只是長度? 在
tails之外,另外記下每格所存元素在輸入中的索引, 以及一個prev[i]指向i被放進來時位在格子l-1的那個索引;再從最後一次 append 沿著prev走回去。tails本身不能直接當答案讀出來(見上面[3,4,5,1]的追蹤)。
相似題目:同樣是 tails + lower bound
| LC # | 題目 | 差異在哪 |
|---|---|---|
| 300 | Longest Increasing Subsequence | 基準題 — bisect_left,答案是 len(tails) |
| 354 | Russian Doll Envelopes | 排序 (w 遞增, h 遞減),再對 heights 做 LIS — 同寬時的 tie 規則正是用來擋掉同寬成鏈 |
| 1964 | Longest Obstacle Course at Each Position | 非遞減 → bisect_right,而且每個位置的答案就是插入位置 + 1,邊走邊回報 |
| 2111 | Minimum Operations to Make the Array K-Increasing | 依 mod k 拆成 k 個剩餘類,各自跑非遞減版;保留 len - LIS |
| 1671 | Minimum Removals to Make a Mountain Array | 從左邊算「結尾在 i」的 LIS + 從右邊算「起點在 i」的 LIS,兩邊都用這個掃描 |
| 646 | Maximum Length of Pair Chain | 同樣是成鏈問題;按結尾值排序後單純貪心就夠了,但 tails 掃描也能解 |
| 1996 | The Number of Weak Characters | 來自 LC 354 的排序 + tie 規則同門技巧,只是不需要二分搜尋 |
陷阱 — 看起來像 LIS,但不屬於這個模式的題目:
| LC # | 題目 | 為什麼 tails 不行 |
|---|---|---|
| 673 | Number of Longest Increasing Subsequence | 要的是數量而不是長度 — 每個長度只存一個結尾,無法帶著重數。改用 O(n²) DP 加一個計數陣列,或對值域開 BIT |
| 368 | Largest Divisible Subset | 「可以接起來」是整除關係,不是全序,所以沒有單一結尾能總結一條鏈 — 用 O(n²) DP + parent pointer |
| 1027 | Longest Arithmetic Subsequence | 狀態是 (index, 差值) — 是 hash map DP,不是一張有序表 |
1.6) 相關演算法與資料結構
互補演算法:
- 雙指標:用於沒有隨機存取能力的已排序序列
- 滑動視窗:用於具備特定性質的子陣列問題
- 遞迴:另一種實作方式
資料結構:
- 陣列:二分搜尋的主要應用場景
- 二元搜尋樹:樹走訪中隱含的二分搜尋
- 雜湊表:不需要排序時的 O(1) 查詢替代方案
2) 二分搜尋模板與模式
補充資源
2.0) 迴圈結束條件比較
關鍵差異:結束條件決定迴圈何時停止,也連帶影響邊界的處理方式。
| 條件 | 邊界型態 | 使用時機 | 主要特徵 |
|---|---|---|---|
while (l <= r) |
閉區間 [l, r] | 標準二分搜尋 | • 最常見的做法 • 搜尋空間同時包含 l 與 r • 需要 l = mid + 1、r = mid - 1 |
while (l < r) |
半開區間 [l, r) | 找邊界 / 插入位置 | • 搜尋空間不含 r • l == r 時迴圈結束• 使用 l = mid + 1、r = mid |
while (l < r - 1) |
保留間隙 | 特殊情況下避免無窮迴圈 | • 確保 l 與 r 永不相鄰 • 迴圈結束後需要再做一次檢查 • 較少見,用於複雜條件 |
詳細分析:
// 1) while (l <= r) - CLOSED BOUNDARY [l, r]
while (l <= r) {
int mid = l + (r - l) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) l = mid + 1; // MUST +1
else r = mid - 1; // MUST -1
}
// Pros: Standard, easy to understand
// Cons: Can return -1 if not found
// 2) while (l < r) - HALF-OPEN [l, r)
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] < target) l = mid + 1; // +1 to exclude mid
else r = mid; // NO -1, keep mid in range
}
// After loop: l == r, points to answer or insertion point
// Pros: Great for finding boundaries, no -1 return
// Cons: Requires different logic for different problems
// 3) while (l < r - 1) - GAP-BASED
while (l < r - 1) {
int mid = l + (r - l) / 2;
if (condition(mid)) l = mid;
else r = mid;
}
// Final check needed: examine both l and r
// Pros: Avoids infinite loops in complex conditions
// Cons: More complex, requires post-processing
各自的使用時機:
while (l <= r):經典二分搜尋,找確切值while (l < r):找第一次 / 最後一次出現的位置、插入位置、找峰值while (l < r - 1):mid 可能等於 l 或 r 的複雜條件
依模式分類的經典 LeetCode 題目
模式 1:while (l <= r) — 精確搜尋
- LC 704: Binary Search(基本實作)
- LC 33: Search in Rotated Sorted Array
- LC 81: Search in Rotated Sorted Array II
- LC 74: Search a 2D Matrix
- LC 240: Search a 2D Matrix II
- LC 69: Sqrt(x)
- LC 367: Valid Perfect Square
- LC 441: Arranging Coins
模式 2:while (l < r) — 找邊界 / 找峰值
- LC 34: Find First and Last Position of Element
- LC 35: Search Insert Position
- LC 162: Find Peak Element
- LC 852: Peak Index in a Mountain Array
- LC 153: Find Minimum in Rotated Sorted Array
- LC 154: Find Minimum in Rotated Sorted Array II
- LC 278: First Bad Version
- LC 658: Find K Closest Elements
- LC 744: Find Smallest Letter Greater Than Target
模式 3:驗證函式類題目(LC 410、875、1011、1060、1482)— 這些題二分搜尋的是 答案,而不是索引;見 binary_search_on_answer.md。
2.1) 標準二分搜尋模板 — LC 704
關鍵原則:
- 初始化:
left = 0, right = nums.length - 1(閉區間) - 迴圈條件:
while (left <= right) - 指標更新:
left = mid + 1、right = mid - 1 - 清晰度小訣竅:所有條件都用
else if,把邏輯攤開講明白
寫程式小訣竅:避免使用
else— 把所有條件都寫成else if,清楚列出每種情況,減少 bug。
// Java Implementation
public int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
// Use <= to search when left == right
while (left <= right) {
int mid = left + (right - left) / 2; // Avoid overflow
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1; // Target in right half
} else { // nums[mid] > target
right = mid - 1; // Target in left half
}
}
return -1; // Not found
}
# Python Implementation
def binary_search(nums, target):
left, right = 0, len(nums) - 1
# Closed boundary [left, right] - includes both endpoints
while left <= right:
mid = left + (right - left) // 2 # Avoid overflow
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1 # Search right half
else:
right = mid - 1 # Search left half
return -1 # Target not found
2.2) 浮點數二分搜尋
用於答案是實數的題目(開根號、最佳分配、幾何問題):
def sqrt(x: float, precision=1e-9) -> float:
lo, hi = 0.0, max(1.0, x)
while hi - lo > precision:
mid = (lo + hi) / 2
if mid * mid <= x:
lo = mid
else:
hi = mid
return lo
# General pattern: binary search on continuous domain
def minimize_real(lo: float, hi: float, iterations=100) -> float:
for _ in range(iterations): # fixed iterations avoids float precision issues
mid = (lo + hi) / 2
if feasible(mid):
hi = mid
else:
lo = mid
return (lo + hi) / 2
2.3) 二維矩陣搜尋 — 兩種不同的題目 (LC 74 / LC 240)
LC 74(矩陣的列與行都排序,值由左到右、由上到下遞增):
def searchMatrix(matrix, target):
m, n = len(matrix), len(matrix[0])
lo, hi = 0, m * n - 1
while lo <= hi:
mid = (lo + hi) // 2
val = matrix[mid // n][mid % n]
if val == target: return True
elif val < target: lo = mid + 1
else: hi = mid - 1
return False
LC 240(每列排序、每行也排序,但整體並未排序 — 用階梯搜尋):
def searchMatrix(matrix, target):
row, col = 0, len(matrix[0]) - 1 # start top-right
while row < len(matrix) and col >= 0:
if matrix[row][col] == target: return True
elif matrix[row][col] > target: col -= 1 # too big → go left
else: row += 1 # too small → go down
return False
關鍵洞見:LC 240 的階梯搜尋每一步消掉一整列或一整行 → O(m+n)。不要把 LC 240 當成攤平後的二分搜尋 — 這個矩陣並非整體有序。
Java — 同樣的攤平做法,完整寫出來:
// java
// LC 74
// V1
// IDEA : BINARY SEARCH + FLATTEN MATRIX
// https://leetcode.com/problems/search-a-2d-matrix/editorial/
public boolean searchMatrix_2(int[][] matrix, int target) {
int m = matrix.length;
if (m == 0)
return false;
int n = matrix[0].length;
// binary search
/** NOTE !!! FLATTEN MATRIX */
int left = 0, right = m * n - 1;
int pivotIdx, pivotElement;
while (left <= right) {
pivotIdx = (left + right) / 2;
/** NOTE !!! TRICK HERE :
*
* pivotIdx / n : y index
* pivotIdx % n : x index
*/
pivotElement = matrix[pivotIdx / n][pivotIdx % n];
if (target == pivotElement)
return true;
else {
if (target < pivotElement)
right = pivotIdx - 1;
else
left = pivotIdx + 1;
}
}
return false;
}
2.4) 雙調 / 山脈陣列 — 遞減順序的翻轉
山脈(雙調)陣列本身沒有排序,但它確實是兩段有序序列的接合, 所以需要三次二分搜尋:先用爬坡搜尋找到峰值,接著在左半段做一般的 遞增搜尋,最後在右半段做遞減搜尋。
遞減順序的模板就是把標準模板的比較方向翻過來:
ascending : nums[mid] < target -> go RIGHT (l = mid + 1)
descending: nums[mid] > target -> go RIGHT (l = mid + 1)
有個漂亮的寫法能把兩者合成一個函式:if ((val < target) == ascending) l = mid + 1; else r = mid - 1;
題解範例 — LC 1095 Find in Mountain Array — 見 binary_search_examples.md。
2.5) No Right End — Exponential (Galloping) Search Priority 4 of 5 — High value — a gap here costs you rounds
Binary search needs a right end, and sometimes the input refuses to give you one: LC 702
(Search in a Sorted Array of Unknown Size) hands you a reader with no length, CtCI 10.4
hands you a Listy whose elementAt() just returns -1 past the end, and a paginated API
behaves the same way. Find a right end by doubling, then binary search inside it.
probe 1, 2, 4, 8, 16, ... until the value overshoots the target (or falls off the end)
-> the answer is inside [prev probe, this probe]
Both halves cost O(log p) probes, where p is the target’s index — so the whole search
is O(log p), independent of the array’s (unknown, possibly huge) total length.
# python
# LC 702 - Search in a Sorted Array of Unknown Size
# IDEA: double the index until reader.get() overshoots -> that bounds the answer;
# then run an ordinary binary search inside the window
# time = O(log p) where p is the target's index, space = O(1)
class Solution(object):
def search(self, reader, target):
hi = 1
while reader.get(hi) < target: # out of range returns 2^31-1, so this stops
hi <<= 1 # 1, 2, 4, 8, ... only O(log p) probes
lo = hi >> 1 # the previous probe was still < target
while lo <= hi:
mid = lo + (hi - lo) // 2
val = reader.get(mid)
if val == target:
return mid
elif val < target:
lo = mid + 1
else:
hi = mid - 1 # also the "past the end" case
return -1
// java
// CtCI 10.4 - search a sorted "Listy" that has no size(); elementAt() returns -1 past the end
// IDEA: same doubling, but the sentinel here is -1, which reads as TOO SMALL —
// so it has to be tested for explicitly instead of comparing naturally
// time = O(log p), space = O(1)
int search(Listy list, int target) {
int hi = 1;
while (list.elementAt(hi) != -1 && list.elementAt(hi) < target) {
hi *= 2;
}
int lo = hi / 2;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int val = list.elementAt(mid);
if (val == target) return mid;
if (val == -1 || val > target) hi = mid - 1; // -1 means past the end -> go left
else lo = mid + 1;
}
return -1;
}
- The trap is the out-of-range sentinel’s direction.
2^31-1sorts above every real value, so the ordinary comparisons handle it.-1sorts below them, so an untested-1sends the search right, off the end, forever. - Why doubling and not a fixed stride. A stride of
kcostsp/kprobes; doubling costslog2(p), and it never overshoots the answer by more than a factor of two. - The same trick outside interviews. Intersecting a small sorted list with a huge one galloping-searches each element instead of scanning, and Timsort’s merge uses it to skip long runs from one side.
2.6) The Probe Can Land on a Hole — Sparse Search Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
CtCI 10.5: a sorted array of strings padded with "" at random positions. mid can land
on an empty string, which carries no order information — you cannot tell whether the
target is left or right of it, so the standard template stalls. Fix: step outward from
mid to the nearest real entry, then compare as usual.
# python
# CtCI 10.5 - find a word in a sorted string array that is padded with "" entries
# IDEA: a probe landing on "" tells you nothing, so walk outward from mid — both
# directions at once — to the closest real string, then continue normally
# time = O(log n) when holes are sparse, O(n) worst case; space = O(1)
def sparse_search(words, target):
if not target: # "" has no defined position
return -1
lo, hi = 0, len(words) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if not words[mid]: # in a hole: find the nearest word
left, right = mid - 1, mid + 1
while True:
if left < lo and right > hi:
return -1 # this whole window is empty
elif right <= hi and words[right]:
mid = right
break
elif left >= lo and words[left]:
mid = left
break
left -= 1
right += 1
if words[mid] == target:
return mid
elif words[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
- Step outward, not just rightward. Scanning only right can run past
hiand force you to discard a half that still contained the target. - The guarantee is gone, and that is the answer. A run of
""of lengthnmakes itO(n); you keepO(log n)only while the holes are sparse. Say this — it is the same lesson as LC 81 (§1.2), wherenums[l] == nums[mid] == nums[r]leaves the probe uninformative and the fallbackl++ / r--degrades the search toO(n).
2.7) Quick Reference — Other Binary-Search-Flavoured Problems
Famous problems that reuse a template already in this doc; listed so you recognise them, no new technique needed.
| LC | Problem | Which template |
|---|---|---|
| 275 | H-Index II | boundary search on index: first i with citations[i] >= n - i (§1.3) |
| 1268 | Search Suggestions System | sort products, lower_bound the growing prefix (§1.3; LC 436 in binary_search_examples.md); Trie is the alternative |
| 349 / 350 | Intersection of Two Arrays I / II | sort the bigger array, binary search each element (hash set / two-pointer alternatives) |
| 792 | Number of Matching Subsequences | per-char sorted index list + upper_bound to jump to the next occurrence (the prefix-sum + lower-bound family — binary_search_examples.md §19) |
| 222 | Count Complete Tree Nodes | binary search the last level’s node index, testing each candidate by walking its bit path — O(log²n) |
| 1044 | Longest Duplicate Substring | binary search on the answer length + Rabin-Karp rolling hash as the predicate (binary_search_on_answer.md) |
| 1385 | Find the Distance Value Between Two Arrays | sort arr2, binary search each arr1[i] for the closest neighbour |
| 1346 | Check If N and Its Double Exist | sort + binary search for 2*x (hash set alternative) |
3) 總結與速查
3.1) 何時該用二分搜尋
✅ 以下情況請用二分搜尋:
- 陣列有序(完全有序、部分有序或旋轉有序)
- 搜尋空間具備單調性
- 需要 O(log n) 的搜尋效能
- 要找邊界或插入位置
- 具備二元性質的最佳化問題
3.2) Template Selection Guide
One table for the whole sheet: given the shape of the input, this is the template to reach for.
| Problem type / input shape | Template | Worked example |
|---|---|---|
| Exact search in a sorted array | Standard closed boundary while l <= r — §2.1 |
LC 704 |
Left boundary (first index >= target) |
Lower bound — §1.3 | LC 34, LC 35, LC 278 |
Right boundary (last index <= target) |
Upper bound − 1 — §1.3 | LC 34, LC 981 |
| Insert position | Lower bound, return l unvalidated — §1.3 |
LC 35 |
| Peak / valley, no target value | Half-open while l < r, r = mid |
LC 162, LC 852 |
| Rotated sorted array | Identify the sorted half — §1.2 | LC 33, LC 81, LC 153, LC 154 |
| Array goes up then down (mountain / bitonic) | Peak + two ordered searches, one descending — §2.4 | LC 1095 |
| Input has no length (reader / stream / paginated API) | Double the index to find a right end, then search — §2.5 | LC 702 |
A probe can land on an uninformative slot ("", duplicates) |
Step outward to the nearest usable entry — §2.6 | CtCI 10.5, LC 81 |
| 2D matrix | Flatten if globally sorted, staircase if only rows+cols sorted — §2.3 | LC 74 vs LC 240 |
| Real-number answer, precision required | Floating-point / fixed-iteration — §2.2 | LC 69 (float variant) |
| “Minimize the maximum” / “maximize the minimum” | Binary search on answer — binary_search_on_answer.md | LC 410, 875, 1011, 1231, 2616 |
| Values in a known range, array NOT sorted | Binary search the value domain + count — binary_search_on_answer.md | LC 287, LC 378 |
| Feasibility needs a graph walk | Binary search on answer + BFS/DFS predicate — binary_search_on_answer.md | LC 1631, LC 778 |
O(n log n) LIS, weighted pick, sorted history |
lower_bound on a sorted array you maintain — §1.5, worked in binary_search_examples.md |
LC 300, LC 354, LC 528, LC 981 |
3.3) 常見陷阱與訣竅
🚫 常見錯誤:
mid = (left + right) / 2的整數溢位 → 改用mid = left + (right - left) / 2- 邊界更新寫錯(
mid與mid ± 1搞混) - 忘了做後續的有效性驗證
while l < r搭配錯誤的更新方式造成無窮迴圈
✅ 最佳實務:
- 一律用
else if讓邏輯清楚 - 邊界搜尋結束後要驗證結果
- 邊界型態要前後一致(閉區間 vs 半開區間)
- 用邊界情況測試:空陣列、單一元素、重複值
3.4) Interview Signals — Which Pattern?
| Signal | Pattern |
|---|---|
| “find minimum/maximum X such that…” | Binary search on answer |
| “sorted array, find first/last occurrence” | Left/right boundary binary search |
| “the smallest value >= X” / “largest <= X”, asked once per element | Sort once + lower/upper bound — §1.3 |
| “the first element to the left/right that is bigger” (positional) | Monotonic stack, NOT binary search — §1.3 |
“the array has no size()” / “the API is paginated” |
Exponential (galloping) search for a right end — §2.5 |
| “matrix with row+col sorted” | Staircase search (NOT flat binary search) |
| “real number answer, precision required” | Floating-point binary search |
“longest increasing/chained subsequence”, O(n²) DP asked to become O(n log n) |
tails + lower bound — §1.5 |
| “can we achieve X?” is monotonic | Binary search on monotonic predicate |
| O(n) solution exists but O(log n) asked | Think: what is the sorted search space? |