问题与目标
排序让数据形成可利用的顺序,查找则根据结构减少无效比较。本篇通过二分查找和归并排序观察算法不变量与复杂度,不建议在业务代码中替代 Python 内置排序。
完成标准:能说明二分查找为什么要求有序,正确处理左右边界,并解释稳定排序、原地排序和复杂度的区别。

二分查找利用有序性,每轮通过中点比较排除一半候选区间。图中的关键不只是“找到目标”,还包括左右边界怎样更新以及搜索区间怎样保持有效。
核心概念
线性查找逐项检查,最坏 O(n);二分查找每轮排除一半,最坏 O(log n),代价是数据必须按同一规则有序。若为了查一次而先排序,总成本通常是 O(n log n),不一定优于一次线性查找。
冒泡、选择、插入排序适合理解局部交换和循环不变量,典型最坏复杂度为 O(n²)。归并排序稳定、时间 O(n log n),需要额外空间。Python sorted() 和 list.sort() 是成熟实现,支持稳定排序和 key 函数。
排序算法怎样比较
| 算法 | 平均时间 | 额外空间 | 稳定性 | 适合理解的重点 |
|---|---|---|---|---|
| 冒泡 | O(n²) | O(1) | 稳定 | 相邻交换 |
| 选择 | O(n²) | O(1) | 通常不稳定 | 每轮选择极值 |
| 插入 | O(n²) | O(1) | 稳定 | 小规模或近乎有序 |
| 归并 | O(n log n) | O(n) | 稳定 | 分治与合并 |
| 快速排序 | 平均 O(n log n) | 递归栈 | 通常不稳定 | 分区与基准值 |
稳定表示相等键的记录保持原相对顺序。它允许先按次要键排序,再稳定地按主要键排序。Python 更推荐一次传入元组键,意图更明确。
二分查找的区间不变量
示例使用闭区间 [left, right]:循环条件是 left <= right,排除中点后必须更新为 middle + 1 或 middle - 1。另一种半开区间写法也正确,但边界规则不能混用。
存在重复值时,普通二分只保证找到某一个位置。寻找第一个或插入点可使用标准库:
from bisect import bisect_left, bisect_right
values = [1, 3, 3, 3, 8]
print(bisect_left(values, 3), bisect_right(values, 3))
输出 1 4,因此相等值区间是 [1, 4)。
可运行实现
from collections.abc import Sequence
def binary_search(values: Sequence[int], target: int) -> int:
left, right = 0, len(values) - 1
while left <= right:
middle = left + (right - left) // 2
if values[middle] == target:
return middle
if values[middle] < target:
left = middle + 1
else:
right = middle - 1
return -1
def merge_sort(values: list[int]) -> list[int]:
if len(values) <= 1:
return values.copy()
middle = len(values) // 2
left = merge_sort(values[:middle])
right = merge_sort(values[middle:])
result: list[int] = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
raw = [8, 3, 5, 3, 9, 1]
ordered = merge_sort(raw)
print("raw:", raw)
print("sorted:", ordered)
print("index:", binary_search(ordered, 5))
print("missing:", binary_search(ordered, 7))
输入包含重复值,输出保留原列表、生成排序结果,并分别返回已找到位置和 -1。归并时使用 <=,相等元素优先取左侧,从而保持稳定性。
业务排序通常直接写:
tasks = [{"id": 2, "priority": 1}, {"id": 1, "priority": 1}]
print(sorted(tasks, key=lambda item: (item["priority"], item["id"])))
sorted() 返回新列表,list.sort() 原地修改并返回 None。大多数业务代码应该使用它们;手写排序用于理解比较、边界和复杂度,不应以教学实现替换成熟库。
进一步验证
- 为二分查找增加空列表、单元素、首尾命中和重复元素测试。
- 要求返回“第一个不小于目标的位置”,重新定义区间不变量,并用
bisect_left对照。 - 用带原始位置的重复值验证归并排序的稳定性。
判断实现可靠性的关键是每次循环后搜索区间都严格缩小,排序结果则与 Python 标准实现一致。
常见问题与排查
- 在未排序数据上二分:先确认排序字段、方向和比较规则一致。
- 边界写成左右都闭合,却更新为
left = middle:可能无法收缩区间而死循环。 - 把索引
0当成未找到:应明确使用-1或None。 - 递归排序不断复制切片:教学实现清楚,但大数据应使用成熟库。
- 比较只看时间:还要说明是否稳定、是否修改原输入和额外空间。
小结
排序与查找的核心不是背代码,而是维护不变量:待查区间持续缩小,归并两侧始终有序。生产代码优先标准实现,手写用于理解边界和评估成本。
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


