Binary Search

Search & SortPriority 5 of 5 — Must know — expect it in almost every loopMust know 更新於 Sep 18, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

Scope — Halving a monotonic search space — the loop-invariant reasoning behind l <= r vs l < r, the boundary (lower/upper bound) templates, rotated arrays, and floating-point and 2D search. See alsodeep dives split out of this file: binary_search_on_answer.md — searching the answer space: the canFinish / isValid predicate, 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)
  • 前提條件:已排序的陣列,或具備單調性
  • 搜尋空間:不限於完全排序的陣列,以下情況都適用:
    • 完全排序的陣列
    • 部分排序的陣列
    • 旋轉排序陣列
    • 任何具備單調性質的空間

核心演算法步驟

  1. 定義邊界:把 leftright 指標初始化成涵蓋所有可能情況
  2. 定義回傳值:想清楚要回傳什麼(索引、值、-1 等)
  3. 定義結束條件:選對迴圈條件(<=<< -1
  4. 更新指標:依與 target 的比較結果移動邊界

何時使用二分搜尋

  • 已排序陣列:找確切值的經典場景
  • 單調函數:只要 condition(k) 能推得 condition(k+1),就能用二分搜尋
  • 搜尋邊界:找某個值第一次 / 最後一次出現的位置
  • 最佳化問題:找滿足限制條件的最小 / 最大值

參考資料

理解二分搜尋的指標行為

核心洞見:lr 到底代表什麼?

這是讓二分搜尋成立的根本概念,也解釋了為什麼像 LC 35 這種求插入位置的題目回傳 l 是對的。

迴圈進行中:搜尋空間不變式

在整個 while (l <= r) 迴圈期間:

  • l 左邊的所有元素都嚴格 < target
  • r 右邊的所有元素都嚴格 > target
  • target 可能出現的位置永遠落在 [l, r] 之內

每次迭代砍掉一半搜尋空間,同時保持這個不變式。

java
// 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

在這一瞬間,陣列被切成兩部分:

text
Visual Representation:

index:     0   ...   r   l   ...   n-1
value:   [< target]  gap  [> target]

迴圈結束時的關鍵性質:

  1. r最後一個小於 target 的元素
  2. l第一個大於等於 target 的元素
  3. rl 之間不存在任何索引(因為 r = l - 1

這就是為什麼 l 正是正確的插入位置!

圖解範例

我們來追蹤 nums = [1, 3, 5, 6], target = 4

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

java
// 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;
}

為什麼不需要特判就能成立:

  1. 若 target 存在:迴圈中就直接回傳 mid
  2. 若 target 不存在:
    • 迴圈以 l > r 結束
    • 依不變式:nums[0..l-1] < targetnums[l..end] >= target
    • 因此 l 正是 target 該被插入的位置

該避開的常見錯誤

java
// ❌ 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 / colscol = 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

模式:找出旋轉點

旋轉排序陣列一定長這個樣子:

text
[Left Higher Plateau] > [Right Lower Plateau]
e.g. [3, 4, 5, 1, 2]
      ^^^^^^^^  ^^^^
      left part  right part (contains minimum)

最小值永遠在旋轉點上 — 也就是陣列中唯一那個「下墜」的位置。

核心想法

判斷 mid 落在哪一段平台上,然後往沒有排序的那一側移動(最小值就在那裡):

text
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 + 1
  • nums[mid] < nums[l] → mid 在右平台上 → 最小值就是 mid 或在其左邊 → r = mid - 1
推薦模板(閉區間邊界,追蹤 ans
java
// 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 變數)
java
// 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]
text
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)

python
# 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     
java
// 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 findLeftl (bisect_left)
“smallest value > X”, “strictly greater” upper bound findRightr + 1 (bisect_right)
“largest value <= X”, “floor”, “most recent before X” upper bound − 1 findRightr (bisect_right - 1)
“largest value < X” lower bound − 1 findLeftl - 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
# 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
// 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:

text
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.

text
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.

模式:兩次獨立的邊界搜尋

有重複值的排序陣列可以看成三個區塊

text
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 走進 elser = mid - 1(往左推) l
findRight nums[mid] <= target 走進 ifl = mid + 1(往右推) r

為什麼這樣成立 — 結束時 lr 已經交錯成 r == l - 1,正好夾住分界點:

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

text
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 的對應關係 — 把這個對照記起來,兩個輔助函式就再也忘不掉:

python
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
# 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
// 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:一個輔助函式,呼叫兩次(targettarget + 1)⭐⭐⭐⭐⭐

最乾淨的技巧 — 只寫 bisect_left,然後注意到 target 最後一次出現的位置 就在 target + 1 第一次出現的位置的前一格:

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

text
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 順便驗證
一個輔助函式,targettarget+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 < hihi = mid(不減 1);r = len(nums)-1 必須搭配 while l <= rr = 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
# 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]

  • appendl == len(tails)):陣列尾端多出 num,且 tails[l-1] < num — 仍然嚴格遞增。
  • overwritel < 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

text
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 不是一個子序列。 只有它的長度有意義:

text
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²) DP dp[i] = max(dp[j]) + 1,而追問要求 O(n log n)

這個推廣值得記住:當一個 DP 的狀態是「每個已達成長度的最佳值」而那張表是單調的,這張表就可以 二分搜尋,整列 DP 轉移也就塌成一次 lower bound 查詢。

差一位的地方 查詢 Python
嚴格遞增 (LC 300) 第一個 >= num 的結尾 bisect_left
非遞減(允許重複) 第一個 > num 的結尾 bisect_right
遞增 / 遞減 把數值取負,再用上面的做法 -numbisect_*

要的是子序列本身,不只是長度?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 + 1r = mid - 1
while (l < r) 半開區間 [l, r) 找邊界 / 插入位置 • 搜尋空間不含 r
l == r 時迴圈結束
• 使用 l = mid + 1r = mid
while (l < r - 1) 保留間隙 特殊情況下避免無窮迴圈 • 確保 l 與 r 永不相鄰
• 迴圈結束後需要再做一次檢查
• 較少見,用於複雜條件

詳細分析:

java
// 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
java
// 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
java
// 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 + 1right = mid - 1
  • 清晰度小訣竅:所有條件都用 else if,把邏輯攤開講明白

寫程式小訣竅:避免使用 else — 把所有條件都寫成 else if,清楚列出每種情況,減少 bug。

java
// 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
# 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) 浮點數二分搜尋

用於答案是實數的題目(開根號、最佳分配、幾何問題):

python
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(矩陣的列與行都排序,值由左到右、由上到下遞增):

python
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(每列排序、每行也排序,但整體並未排序 — 用階梯搜尋):

python
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
// 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) 雙調 / 山脈陣列 — 遞減順序的翻轉

山脈(雙調)陣列本身沒有排序,但它確實是兩段有序序列的接合, 所以需要三次二分搜尋:先用爬坡搜尋找到峰值,接著在左半段做一般的 遞增搜尋,最後在右半段做遞減搜尋。

遞減順序的模板就是把標準模板的比較方向翻過來:

text
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.

text
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
# 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
// 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-1 sorts above every real value, so the ordinary comparisons handle it. -1 sorts below them, so an untested -1 sends the search right, off the end, forever.
  • Why doubling and not a fixed stride. A stride of k costs p/k probes; doubling costs log2(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
# 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 hi and force you to discard a half that still contained the target.
  • The guarantee is gone, and that is the answer. A run of "" of length n makes it O(n); you keep O(log n) only while the holes are sparse. Say this — it is the same lesson as LC 81 (§1.2), where nums[l] == nums[mid] == nums[r] leaves the probe uninformative and the fallback l++ / r-- degrades the search to O(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
  • 邊界更新寫錯(midmid ± 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?