🏠 总目录📚 本教程 04 · 二分、排序与堆:怎样少看一半 ← →
📑 本页目录(点开跳转)

04 · 二分、排序与堆:怎样少看一半

⏱ 8 分钟 | 边界二分、旋转数组最小值、最小的 k 个数


🎯 一句话

二分不是“猜中间数”,而是每一轮都能确定:答案还在左半还是右半。

🧩 题 1:升序数组中数字出现的次数

不要一个个数。找第一次出现的位置,再找第一次“大于目标”的位置;两者相减就是次数。

def lower_bound(nums, target):
    left, right = 0, len(nums)
    while left < right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid
    return left


def count_target(nums, target):
    return lower_bound(nums, target + 1) - lower_bound(nums, target)


assert count_target([1, 2, 2, 2, 3, 4], 2) == 3
assert count_target([1, 2, 3], 9) == 0

不变量:答案永远在半开区间 [left, right) 中;right 不是最后一个合法下标,而是“答案可能到这里为止”的边界。

🧩 题 2:旋转数组的最小数字

[3, 4, 5, 1, 2] 看起来乱了,但中点总能和右端比较:中点更大,最小值在右边;中点更小,最小值在左边或就是中点。

def min_in_rotated(nums):
    left, right = 0, len(nums) - 1
    while left < right:
        mid = (left + right) // 2
        if nums[mid] > nums[right]:
            left = mid + 1
        else:
            right = mid
    return nums[left]


assert min_in_rotated([3, 4, 5, 1, 2]) == 1
assert min_in_rotated([1, 2, 3]) == 1

🧩 题 3:最小的 k 个数

当你只要前 k 个,不必把全部排好。Python 的最小堆直接拿前 k 个最小元素。

import heapq


def smallest_k(nums, k):
    if not 0 <= k <= len(nums):
        raise ValueError("k out of range")
    return heapq.nsmallest(k, nums)


assert smallest_k([4, 5, 1, 6, 2, 7, 3, 8], 4) == [1, 2, 3, 4]

⚠️ 面试若要求手写,可用“大小为 k 的最大堆”;但先把“只维护 k 个候选”这个思想弄懂。


🔗 这一章连到哪里

去哪 为什么
不靠数据的 AI 07 · 启发式与 A* ⭐ 题 3 的 heapq 换个场景就是 A* 的心脏:按 $f = g + h$ 最小取出下一个节点,用的就是同一个最小堆

🛑 可以停在这里

二分每轮都删掉一半,前提是你说得出删掉那一半为什么不可能有答案。


✅ 检查点

为什么 lower_bound 的右边界写成 len(nums) 而不是 len(nums)-1?

👀 看答案

因为要表达“目标比所有元素都大”时的插入位置;半开区间也让空数组和边界判断统一。


⚡ 走神救援

继续: 05 · 动态规划:把今天交给昨天

打卡记录保存在你的浏览器里,首页能看到总进度