二分搜尋 — 實戰題解

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

範圍 — binary_search.md 的題組:每題歸檔在它所歸約的索引空間模板底下,寫成一張題目卡、一份標準解,另外收錄少數沒在 binary_search_on_answer.md 解過的答案空間題。 另見 — 主文件:binary_search.md — 迴圈不變式、邊界模板,以及「該用哪個模板」的決策表;binary_search_on_answer.md — 所有對答案而非索引做二分搜尋的題目。 鄰近文件:n_sum.md — LC 167 的收斂指標解法;sort.md — LC 354 的先排序再 LIS 解法;patience_sorting.md — 歸約到 LIS 的題目;matrix.md — 二維網格題型家族。

LeetCode 題目清單

概覽

binary_search.md 收著八個索引空間模板;這一頁是題目。下面每一節對應一個模板, 順序與主文件相同,每張卡片只說明它比模板多出的那一步。錨定題本身就是全部重點的模板 (模板 4、6、8)在這裡沒有獨立一節。最後一節收錄歸約到 binary_search_on_answer.md 卡片的答案空間題,指標表則列出在其他文件解過的題目。

索引

在別處解 — 只解一次,放在擁有它的那份文件:

LC 題目 所在位置 為什麼在那裡
162 Find Peak Element binary_search.md § Template 7 該模板的錨定題
300 Longest Increasing Subsequence binary_search.md § Template 5 該模板的錨定題
354 Russian Doll Envelopes sort.md § Template 9 重點是帶平手規則的排序;之後的 LIS 就是模板 5
167 Two Sum II — Input Array Is Sorted n_sum.md 雙指標是 O(n);每個元素各做一次二分搜尋是 O(n log n)
209 Minimum Size Subarray Sum sliding_window.md § Template 4 滑動視窗是 O(n);前綴和 + 每個起點一次 lower bound 是 O(n log n) 的追問
69 Sqrt(x) math.md § Integer Square Root 對答案二分搜尋與牛頓法並排
528 Random Pick with Weight math.md § Randomized Sampling 前綴和 + 對均勻抽樣做 lower bound
981 Time Based Key-Value Store design_examples.md 每個 key 一份排序清單,用 upper bound − 1 查詢
287 Find the Duplicate Number binary_search_on_answer.md § Find the Duplicate Number 在值域上二分搜尋,配合鴿籠計數

精確比對範例

Median of Two Sorted Arrays — LC 4 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

關鍵 — 搜尋的是較短陣列裡的切點,不是某個值。nums1 的切點 i 決定了 nums2 的切點 j = half − i;當兩個左側最大值都 <= 兩個右側最小值時切點就對了,切錯時,看是哪個不等式 不成立就知道該往哪邊移。

歸約到:binary_search § Template 1 — 同一個閉區間迴圈、三種結果(找到、往左、往右),只是搜尋的是切點位置 [0, m] 而不是索引,判斷用跨陣列的比較而不是 nums[mid] == target。

python
# python
# LC 4 - Median of Two Sorted Arrays
# IDEA: binary search the cut i in the shorter array; j = half - i; stop when left maxima <= right minima
# time = O(log(min(m, n))), space = O(1)
def findMedianSortedArrays(nums1, nums2):
    if len(nums1) > len(nums2):
        nums1, nums2 = nums2, nums1          # search the shorter array
    m, n = len(nums1), len(nums2)
    half = (m + n + 1) // 2                  # size of the left half (the larger one when odd)
    left, right = 0, m                       # i = how many of nums1 go to the left half
    while left <= right:
        i = left + (right - left) // 2
        j = half - i
        left1 = nums1[i - 1] if i > 0 else float('-inf')
        right1 = nums1[i] if i < m else float('inf')
        left2 = nums2[j - 1] if j > 0 else float('-inf')
        right2 = nums2[j] if j < n else float('inf')
        if left1 <= right2 and left2 <= right1:          # a valid cut
            if (m + n) % 2 == 1:
                return max(left1, left2)
            return (max(left1, left2) + min(right1, right2)) / 2
        if left1 > right2:
            right = i - 1                    # took too many from nums1
        else:
            left = i + 1                     # took too few from nums1
    return 0.0                               # unreachable for sorted input
java
// java
// LC 4 - Median of Two Sorted Arrays
// IDEA: binary search the cut in the shorter array; the other cut follows from the half size
// time = O(log(min(m, n))), space = O(1)
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    if (nums1.length > nums2.length) return findMedianSortedArrays(nums2, nums1);
    int m = nums1.length, n = nums2.length;
    int half = (m + n + 1) / 2;
    int left = 0, right = m;
    while (left <= right) {
        int i = left + (right - left) / 2;
        int j = half - i;
        int left1  = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
        int right1 = i == m ? Integer.MAX_VALUE : nums1[i];
        int left2  = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
        int right2 = j == n ? Integer.MAX_VALUE : nums2[j];
        if (left1 <= right2 && left2 <= right1) {
            if ((m + n) % 2 == 1) return Math.max(left1, left2);
            return (Math.max(left1, left2) + (double) Math.min(right1, right2)) / 2.0;
        } else if (left1 > right2) {
            right = i - 1;
        } else {
            left = i + 1;
        }
    }
    return 0.0;
}

陷阱

  • 搜尋較長的陣列會讓 j = half − i 變成負數:nums1 = [1, 2, 3, 4]、nums2 = [5] 可能探測到 i = 4,於是 j = −1。先交換,讓 nums1 是較短的那個。
  • 空的一邊需要 ±inf 哨兵:nums1 = []、nums2 = [1] 時 i = 0,沒有 nums1[i − 1]。
  • Java 中兩個大 int 的 max + min 會在除法前溢位:以 double 相加。

相似題 — LC 2040 (第 k 小的乘積:binary_search_on_answer § Template 4)

下界與上界範例

First Bad Version — LC 278

關鍵 — isBadVersion 讀起來是 False … False True … True,所以第一個壞版本就是模板 2 的 findLeft,比較換成呼叫 API。沒有陣列,也沒有 target。

歸約到:binary_search § Template 2 — 把 nums[mid] >= target 換成 isBadVersion(mid) 的 findLeft;答案一定存在,所以不用驗證。

python
# python
# LC 278 - First Bad Version
# IDEA: lower bound on the boolean API: the first version where isBadVersion turns True
# time = O(log n), space = O(1)
def firstBadVersion(n, isBadVersion):
    left, right = 1, n
    while left < right:
        mid = left + (right - left) // 2
        if isBadVersion(mid):
            right = mid              # mid is bad: the first bad one is mid or earlier
        else:
            left = mid + 1           # mid is good: the first bad one is after it
    return left
java
// java
// LC 278 - First Bad Version
// IDEA: lower bound on the boolean API
// time = O(log n), space = O(1)
public int firstBadVersion(int n) {
    int left = 1, right = n;
    while (left < right) {
        int mid = left + (right - left) / 2;   // (left + right) overflows when n is near 2^31
        if (isBadVersion(mid)) right = mid;
        else left = mid + 1;
    }
    return left;
}

陷阱

  • Java 中 (left + right) / 2 會溢位:n = 2126753390 就是 LeetCode 自己的測資。

相似題 — LC 374 (猜數字 API,精確比對) · LC 35 (插入位置)

Find Smallest Letter Greater Than Target — LC 744

關鍵 — 「嚴格大於 target 的最小字母」是 upper bound(第一個 > target 的字母)。 沒有更大的字母時答案繞回 letters[0],% n 直接處理掉。

歸約到:binary_search § Template 2 — 在字元上做 findRight + 1(upper bound),再把索引繞回。

python
# python
# LC 744 - Find Smallest Letter Greater Than Target
# IDEA: upper bound (first letter > target) on a half-open range, then wrap with % n
# time = O(log n), space = O(1)
def nextGreatestLetter(letters, target):
    left, right = 0, len(letters)            # half-open [left, right): right == n means "none"
    while left < right:
        mid = left + (right - left) // 2
        if letters[mid] <= target:
            left = mid + 1                   # not strictly greater: the answer is to the right
        else:
            right = mid
    return letters[left % len(letters)]      # left == n wraps around to letters[0]

陷阱

  • 用 letters[mid] < target 找到的是第一個 >= target 的字母:letters = ["c","f","j"]、 target = "c" 會回傳 "c" 而不是 "f"。
  • 允許重複:["e","e","e","n"]、target = "e" 必須跳過每個 "e",<= 就能做到。

Check If a Number Is Majority Element in a Sorted Array — LC 1150

關鍵 — 在排序陣列裡,同一個值佔據一段連續區間。找出 target 那段的起點; 它是多數元素,恰好當往後 n // 2 個位置的元素仍是 target。

歸約到:binary_search § Template 2 — 一次 findLeft,接著一次 O(1) 檢查,取代第二次搜尋。

python
# python
# LC 1150 - Check If a Number Is Majority Element in a Sorted Array
# IDEA: lower bound = start of target's run; majority iff nums[start + n // 2] == target
# time = O(log n), space = O(1)
def isMajorityElement(nums, target):
    n = len(nums)
    left, right = 0, n
    while left < right:                      # first index with nums[i] >= target
        mid = left + (right - left) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid
    end = left + n // 2                      # a run longer than n / 2 must still cover this index
    return end < n and nums[end] == target
java
// java
// LC 1150 - Check If a Number Is Majority Element in a Sorted Array
// IDEA: lower bound = start of target's run; majority iff nums[start + n / 2] == target
// time = O(log n), space = O(1)
public boolean isMajorityElement(int[] nums, int target) {
    int n = nums.length;
    int left = 0, right = n;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) left = mid + 1;
        else right = mid;
    }
    int end = left + n / 2;
    return end < n && nums[end] == target;
}
text
nums = [2,4,5,5,5,5,5,6,6], target = 5, n = 9
lower bound of 5 -> index 2
index 2 + 9 // 2 = 6, nums[6] = 5 == target -> true

index:  0  1  2  3  4  5  6  7  8
value:  2  4 [5] 5  5  5 [5] 6  6
             start        ^ still 5 iff the run is longer than n / 2

陷阱

  • 漏掉 end < n 的檢查:nums = [1, 2]、target = 2 起點是 1,1 + 1 = 2 已經越界。
  • target 不存在時:lower bound 落在別的值上,檢查仍回傳 False,不需要另外判斷「有沒有找到」。

相似題 — LC 34 (區間的兩端) · LC 169 (未排序的多數元素:Boyer–Moore)

Single Element in a Sorted Array — LC 540

關鍵 — 在單一元素之前,每一對都從偶數索引開始;在它之後則從奇數索引開始。所以 「偶數 mid 那一對完整」是 True … True False … False,單一元素就是第一個不成立的偶數索引。

歸約到:binary_search § Template 2 — 對奇偶判定式做 lower bound,只探測偶數索引。

python
# python
# LC 540 - Single Element in a Sorted Array
# IDEA: probe even mids; an intact pair (nums[mid] == nums[mid+1]) means the single is to the right
# time = O(log n), space = O(1)
def singleNonDuplicate(nums):
    left, right = 0, len(nums) - 1
    while left < right:
        mid = left + (right - left) // 2
        if mid % 2 == 1:
            mid -= 1                         # align mid to the start of a pair
        if nums[mid] == nums[mid + 1]:
            left = mid + 2                   # pair intact: the single is after this pair
        else:
            right = mid                      # pair broken: the single is at mid or before
    return nums[left]
java
// java
// LC 540 - Single Element in a Sorted Array
// IDEA: probe even mids; an intact pair means the single is to the right
// time = O(log n), space = O(1)
public int singleNonDuplicate(int[] nums) {
    int left = 0, right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (mid % 2 == 1) mid--;
        if (nums[mid] == nums[mid + 1]) left = mid + 2;
        else right = mid;
    }
    return nums[left];
}

陷阱

  • 不把 mid 對齊到偶數,nums[mid] == nums[mid + 1] 可能比到一對的後半: [1,1,2,3,3,4,4] 探測 mid = 3,看到 3 == 3,跳到 left = 5,回傳 4 而不是 2。
  • mid + 1 一定在範圍內,因為 left < right 且 right 永遠是偶數(長度是奇數)。

相似題 — LC 136 (未排序:O(n) 的 XOR)

Find K Closest Elements — LC 658

關鍵 — 答案是一個視窗 arr[i .. i + k − 1],所以搜尋視窗的起點。比較平移時會互換的 兩個元素:若 x − arr[i] > arr[i + k] − x,從 i + 1 開始的視窗至少一樣近,而這個判斷只會從 True 轉成 False 一次。

歸約到:binary_search § Template 2 — 在視窗起點 [0, n − k] 上做 lower bound,判定式是「往右移不會更好」。

python
# python
# LC 658 - Find K Closest Elements
# IDEA: binary search the window start i in [0, n - k]; move right while arr[i] is farther than arr[i + k]
# time = O(log(n - k) + k), space = O(1) besides the output
def findClosestElements(arr, k, x):
    left, right = 0, len(arr) - k
    while left < right:
        mid = left + (right - left) // 2
        if x - arr[mid] > arr[mid + k] - x:
            left = mid + 1                   # arr[mid] is farther than arr[mid + k]: start later
        else:
            right = mid                      # arr[mid] is at least as close (ties keep the smaller)
    return arr[left:left + k]

陷阱

  • 用 abs(x - arr[mid]) > abs(arr[mid + k] - x),在兩者都在 x 同一側時會出錯: arr = [1,1,2,2,2,2,2,3,3]、k = 3、x = 3 回傳 [2,2,2] 而不是 [2,3,3]。保留帶正負號的差值。
  • 用 >= 取代 > 會在平手時偏向較大的值;題目要較小的: arr = [1,2,3,4,5]、k = 4、x = 3 必須回傳 [1,2,3,4]。

第二種做法 — 從兩端往內縮(丟掉比較遠的那端)直到剩 k 個:O(n − k),想不出二分判定式時 就給這個版本。依距離排序(sort.md)是 O(n log n)。

Find Right Interval — LC 436 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

關鍵 — 對每個區間,答案是所有區間中最小的 start >= end_i:在排序後的 start 上做 lower bound。答案必須是原始索引,所以排序 (start, index) 配對,再從搜尋落點的配對讀出索引。

歸約到:binary_search § Template 2 — 每個區間一次 findLeft,在連同原始索引一起排序的 start 上搜尋。

python
# python
# LC 436 - Find Right Interval
# IDEA: sort (start, original index) once; per interval, lower-bound end_i among the starts
# time = O(n log n), space = O(n)
import bisect

def findRightInterval(intervals):
    pairs = sorted((start, i) for i, (start, _) in enumerate(intervals))
    starts = [start for start, _ in pairs]           # bisect needs a plain sorted list
    result = []
    for _, end in intervals:
        j = bisect.bisect_left(starts, end)          # findLeft: first start >= end
        result.append(pairs[j][1] if j < len(pairs) else -1)
    return result
text
intervals = [[3,4],[2,3],[1,2]]          original indices 0, 1, 2
pairs     = [(1,2), (2,1), (3,0)]        sorted by start
[3,4]: end 4 -> no start >= 4            -> -1
[2,3]: end 3 -> first start >= 3 = (3,0) -> 0
[1,2]: end 2 -> first start >= 2 = (2,1) -> 1

陷阱

  • 直接排序區間本身會弄丟答案需要的索引:在排序之前把每個 start 和它的索引配成一對。
  • 區間可以是自己的右區間:[[1,1]] 回傳 [0](start 1 >= end 1)。
  • 雙層迴圈是 O(n²),在 n = 2·10⁴ 時會超時;重點就是每個區間只做一次 O(log n) 搜尋。

相似題 — LC 35 (最單純的 lower bound) · LC 2300 (每個 spell 一次 lower bound,數後綴) · LC 1268 (對逐步變長的前綴做 lower bound)

旋轉陣列最小值範例

Find Minimum in Rotated Sorted Array II — LC 154

關鍵 — 重複值帶來第三種情況:nums[mid] == nums[right] 無法判斷 mid 在哪一段,但此時 nums[right] 不是唯一的最小值(nums[mid] 等於它),所以 right -= 1 丟掉它是安全的。 最壞情況變成 O(n)。

歸約到:binary_search § Template 3 — 同樣是對 nums[right] 的半開擠壓,相等時多一步縮一格。

python
# python
# LC 154 - Find Minimum in Rotated Sorted Array II
# IDEA: compare mid with right; on equality the right end is a duplicate, so drop it
# time = O(log n) average, O(n) worst case (all equal); space = O(1)
def findMin(nums):
    left, right = 0, len(nums) - 1
    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] < nums[right]:
            right = mid                      # mid is in the right run: min is at mid or left of it
        elif nums[mid] > nums[right]:
            left = mid + 1                   # mid is in the left run: min is after mid
        else:
            right -= 1                       # equal: nums[right] has a copy at mid, drop it
    return nums[left]
java
// java
// LC 154 - Find Minimum in Rotated Sorted Array II
// IDEA: compare mid with right; on equality drop the right end
// time = O(log n) average, O(n) worst case; space = O(1)
public int findMin(int[] nums) {
    int left = 0, right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < nums[right]) right = mid;
        else if (nums[mid] > nums[right]) left = mid + 1;
        else right--;
    }
    return nums[left];
}

陷阱

  • 改成和 nums[left] 比較:[1, 3, 3] 沒有旋轉,nums[mid] >= nums[left] 把搜尋送往右邊, 錯過索引 0 的 1。
  • O(n) 最壞情況是真的:[1,1,1,1,0,1,1] 任何探測都無法判斷。被問到時要說出來。

相似題 — LC 81 (含重複值的搜尋:在模板 4 上用同樣的 right -= 1 退路)

找峰值範例

Peak Index in a Mountain Array — LC 852

關鍵 — 山形陣列恰好只有一個峰,所以模板 7 不用改:往 mid 較高的鄰居走,直到視窗只剩一個索引。

歸約到:binary_search § Template 7 — 原封不動;山形保證讓峰值唯一。

python
# python
# LC 852 - Peak Index in a Mountain Array
# IDEA: half-open squeeze toward the higher neighbour; the window always holds the peak
# time = O(log n), space = O(1)
def peakIndexInMountainArray(arr):
    left, right = 0, len(arr) - 1
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] < arr[mid + 1]:
            left = mid + 1                   # rising: the peak is to the right
        else:
            right = mid                      # falling: the peak is mid or to the left
    return left

相似題 — LC 162 (任意峰值,沒有山形保證) · LC 1095 (見下) · LC 1901 (二維峰值)

Find in Mountain Array — LC 1095 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

關鍵 — 山形陣列是兩段排序區間在峰頂接起來。用模板 7 找出峰值,接著先搜尋遞增的左段 (索引最小的勝出),之後才搜尋遞減的右段,比較方向要翻轉。三次 O(log n) 搜尋能維持在 get() 的 100 次呼叫上限之內。

歸約到:binary_search § Template 7 — 先找峰值,再做兩次模板 1:左段照原樣,右段把方向翻轉。

python
# python
# LC 1095 - Find in Mountain Array
# IDEA: peak via the slope search, then an ascending search on [0, peak] and a descending one on [peak + 1, n - 1]
# time = O(log n), space = O(1)
class Solution:
    def findInMountainArray(self, target, mountain_arr):
        n = mountain_arr.length()

        left, right = 0, n - 1                       # 1) the peak
        while left < right:
            mid = left + (right - left) // 2
            if mountain_arr.get(mid) < mountain_arr.get(mid + 1):
                left = mid + 1
            else:
                right = mid
        peak = left

        index = self.search(mountain_arr, target, 0, peak, True)       # 2) ascending half first
        if index != -1:
            return index
        return self.search(mountain_arr, target, peak + 1, n - 1, False)  # 3) descending half

    def search(self, arr, target, left, right, ascending):
        while left <= right:
            mid = left + (right - left) // 2
            value = arr.get(mid)
            if value == target:
                return mid
            if (value < target) == ascending:        # ascending: too small -> right; descending: too big -> right
                left = mid + 1
            else:
                right = mid - 1
        return -1
java
// java
// LC 1095 - Find in Mountain Array
// IDEA: peak via the slope search, then an ascending and a descending Template 1 search
// time = O(log n), space = O(1)
class Solution {
    public int findInMountainArray(int target, MountainArray arr) {
        int n = arr.length();
        int left = 0, right = n - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (arr.get(mid) < arr.get(mid + 1)) left = mid + 1;
            else right = mid;
        }
        int peak = left;

        int index = search(arr, target, 0, peak, true);
        if (index != -1) return index;
        return search(arr, target, peak + 1, n - 1, false);
    }

    private int search(MountainArray arr, int target, int left, int right, boolean ascending) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            int value = arr.get(mid);
            if (value == target) return mid;
            if ((value < target) == ascending) left = mid + 1;
            else right = mid - 1;
        }
        return -1;
    }
}
text
arr = [1,2,3,4,5,3,1], target = 3
peak search  -> peak = 4 (value 5)
ascending  [0, 4]: mid 2 -> 3 == target -> return 2     (index 5 also holds 3, but 2 is smaller)

陷阱

  • 先搜右段會回傳較大的索引:[1,2,3,4,5,3,1]、target = 3 必須是 2,不是 5。
  • 在右段沿用遞增的比較會走錯方向:[0,5,3,1]、target = 1 永遠找不到。
  • 線性掃描找峰值在 n = 10⁴ 時會超出呼叫額度。

相似題 — LC 852 (只找峰值) · LC 33 (被旋轉切開的兩段排序區間:binary_search § Template 4)

對答案搜尋範例

Valid Perfect Square — LC 367

關鍵 — 搜尋的是根,不是陣列:[1, num] 上第一個讓 r * r >= num 的 r,就是單調判定式的 第一個 True。num 是完全平方數,恰好當那個 r 的平方等於它。

歸約到:binary_search_on_answer § Template 1 — 用 feasible(r) = r * r >= num 找最小可行值,再做一次相等檢查。

python
# python
# LC 367 - Valid Perfect Square
# IDEA: first r in [1, num] with r * r >= num; a perfect square iff r * r == num
# time = O(log num), space = O(1)
def isPerfectSquare(num):
    left, right = 1, num
    while left < right:
        mid = left + (right - left) // 2
        if mid * mid >= num:
            right = mid                      # mid is big enough: the root is mid or smaller
        else:
            left = mid + 1
    return left * left == num
java
// java
// LC 367 - Valid Perfect Square
// IDEA: first r with r * r >= num, in long to avoid overflow
// time = O(log num), space = O(1)
public boolean isPerfectSquare(int num) {
    long left = 1, right = num;
    while (left < right) {
        long mid = left + (right - left) / 2;
        if (mid * mid >= num) right = mid;
        else left = mid + 1;
    }
    return left * left == num;
}

陷阱

  • Java 中 int 的 mid * mid 在 mid > 46340 時溢位:num = 2147483647 會探測到 mid ≈ 10⁹。用 long。
  • 題目禁止用函式庫的 sqrt;牛頓迭代是另一個被接受的答案 (math.md § Integer Square Root)。

相似題 — LC 69 (下取整的根:最後一個 r * r <= x 的 r) · LC 633 (兩個平方數之和為 c)

Missing Element in Sorted Array — LC 1060

關鍵 — missing(i) = nums[i] − nums[0] − i 計算索引 i 之前缺了幾個數,而且絕不會變小。 找出最後一個缺數少於 k 的索引;答案就是從 nums[i] 再往後數那麼多步。

歸約到:binary_search_on_answer § Template 1 — 在索引上找第一個 missing(i) >= k;和那份文件裡的 LC 1539 是同一個推導計數技巧。

python
# python
# LC 1060 - Missing Element in Sorted Array
# IDEA: missing(i) = nums[i] - nums[0] - i is monotone; find the first index with missing(i) >= k
# time = O(log n), space = O(1)
def missingElement(nums, k):
    def missing(i):
        return nums[i] - nums[0] - i         # numbers absent between nums[0] and nums[i]

    left, right = 0, len(nums)               # right = n: the answer may lie past the last element
    while left < right:
        mid = left + (right - left) // 2
        if missing(mid) >= k:
            right = mid
        else:
            left = mid + 1
    # left = first index with >= k missing, so index left - 1 has < k; the answer is past nums[left - 1]
    return nums[left - 1] + k - missing(left - 1)
text
nums = [4,7,9,10], k = 3
missing:  [0, 2, 3, 3]
first index with missing >= 3 -> 2
answer = nums[1] + 3 - missing(1) = 7 + 3 - 2 = 8      (missing numbers: 5, 6, 8)

陷阱

  • 第 k 個缺數可能落在陣列之後:[4,7,9,10]、k = 5 → 12。right = len(nums) 涵蓋得到; right = len(nums) − 1 則不行。
  • left 永遠不會是 0,因為對每個 k >= 1 都有 missing(0) = 0 < k,所以 left − 1 是安全的。

相似題 — LC 1539 (從 1 起算的缺失正整數) · LC 275 (H-Index II:推導出的計數對上索引)

Arranging Coins — LC 441

關鍵 — k 個完整的列需要 k (k + 1) / 2 枚硬幣,隨 k 遞增。答案是仍放得下的最大 k, 也就是 True … True False … False 的最後一個 True。

歸約到:binary_search_on_answer § Template 2 — 用 feasible(k) = k (k + 1) / 2 <= n 找最大可行值,偏上的 mid。

python
# python
# LC 441 - Arranging Coins
# IDEA: last k in [0, n] with k * (k + 1) // 2 <= n; upper mid so left = mid always moves
# time = O(log n), space = O(1)
def arrangeCoins(n):
    left, right = 0, n
    while left < right:
        mid = left + (right - left + 1) // 2  # upper mid
        if mid * (mid + 1) // 2 <= n:
            left = mid                        # mid rows fit: the answer is mid or more
        else:
            right = mid - 1
    return left
java
// java
// LC 441 - Arranging Coins
// IDEA: last k with k * (k + 1) / 2 <= n, in long; upper mid
// time = O(log n), space = O(1)
public int arrangeCoins(int n) {
    long left = 0, right = n;
    while (left < right) {
        long mid = left + (right - left + 1) / 2;
        if (mid * (mid + 1) / 2 <= n) left = mid;
        else right = mid - 1;
    }
    return (int) left;
}

陷阱

  • 偏下的 mid 搭配 left = mid,一旦 right = left + 1 就永遠迴圈:n = 1 會卡在 left = 0, right = 1。
  • mid * (mid + 1) 在 n 接近 2³¹ 時溢位 int:用 long。

相似題 — LC 69 (同樣是「成本放得下的最大 k」) · LC 2226 (最大可行的每堆數量)