顺序、链式与二分查找
本节目标
理解顺序结构为何擅长按位置访问、链式结构为何擅长在已知节点附近改链接,并能在“始终保持排序”的小型序列上正确使用 bisect。重点是操作取舍,不要求手写生产链表。
线性结构只有一个前后方向
在线性结构中,元素按一个序列排列。常见操作包括:
- 按下标取得第 项;
- 在开头、结尾或中间插入/删除;
- 从头到尾遍历;
- 按值查找;
- 维护排序或只保留一个窗口。
“有顺序”不等于“已经排序”。消息列表按到达顺序排列;优先级序列按某个键排序;字典保持插入顺序但不是按键自动排序。
Python list 的实用模型
对常见 CPython,可以把 list 直觉化为可增长的对象引用数组:
- 按下标定位通常为 ;
- 尾部
append通常为摊还 ; - 中间/头部插入删除要移动后续引用,通常为 ;
value in items可能逐项比较,为 ;- 切片产生新的外层列表,代价随切片长度增长。
这是有用的实现成本模型,不是 Python 语言规范对每个实现的固定内存布局承诺。不要在教材中写死“每项多少 bytes”。
messages = ["system", "user", "assistant"]
first = messages[0]
tail = messages[1:] # 新外层列表
messages.append("tool") # 原列表尾部更新链式结构的取舍
链表节点保存值和到下一节点(双向链表还含上一节点)的引用。若已持有目标节点,修改附近链接可为 ;但要找到第 项通常仍需从一端走 。
[A | next] → [B | next] → [C | None]在普通 Python 业务代码中,手写链表常带来额外对象、引用和边界 bug,局部性也可能比顺序存储差。需要两端队列时优先 collections.deque;只有数据结构练习或确有节点级拼接需求时才实现链表,并用基准和不变量证明价值。
排序序列与二分边界
若序列已经按同一规则排序,二分法每次排除一半范围,查找边界为 。Python bisect 返回插入点,不直接判断相等:
from bisect import bisect_left, bisect_right
timestamps = [10, 20, 20, 35]
left = bisect_left(timestamps, 20) # 1:第一个 20 之前
right = bisect_right(timestamps, 20) # 3:最后一个 20 之后
print(timestamps[left:right]) # [20, 20]查找精确值时仍要检查边界与相等:
def find_exact(sorted_values: list[int], target: int) -> int | None:
index = bisect_left(sorted_values, target)
if index != len(sorted_values) and sorted_values[index] == target:
return index
return Nonebisect 只依赖 < 来定位,不会验证输入真的有序。若排序键、大小写规则或数据在查找期间改变,结果不再可信。
insort 不是 O(log n) 插入
from bisect import insort
latencies = [80, 120, 250]
insort(latencies, 100)
print(latencies) # [80, 100, 120, 250]定位插入点为 ,但列表必须移动后续元素,所以 insort 总体通常为 。它适合小型、读取较多、偶尔插入的有序集合。若频繁按唯一键查找,用 dict;若只需反复取得最小优先级,用堆;若数据量或并发写入很大,应选择专门索引/存储。
官方文档也明确指出 bisect 函数不是线程安全的;并发修改同一序列可能得到未定义或失序结果。
Agent 工程例子
- 用
bisect_left找日志时间窗口的左边界; - 用
bisect_right切出不晚于某阈值的已排序分数; - 小型有序候选集插入;
- 不用它替代向量索引、数据库索引或优先级队列。
练习
- 对
[10, 20, 20, 35]手算目标5、20、40的左右插入点,再运行验证。 - 写
within_range(sorted_values, low, high),返回位于闭区间的切片且不修改输入。 - 比较列表和链表对“按下标读、已知节点后插入、整体遍历”的成本。
- 故意传入未排序列表,说明为什么“函数没有报错”不能证明结果正确。
- 为
find_exact写空列表、首项、末项、重复值和不存在值测试。
自测
- 能区分保序和排序。
- 能解释 Python
list中间插入为何通常要移动元素。 - 能说明链表 O(1) 插入隐含“已找到节点”的前提。
- 能正确区分
bisect_left与bisect_right。 - 不会把
insort的总成本说成 。
下一步与相关概念
参考资料
获取日期:2026-07-14。