数组、链表描述存储和连接方式,栈、队列描述操作约束。栈可以由动态数组实现,队列也可以由环形数组或链式结构实现;接口相同不代表成本相同。
访问模式比名字更重要
连续数组按下标定位元素,支持随机访问,插入中间位置通常需要搬动后续元素。Python list 是可变长度的序列容器,适合尾部追加与弹出;pop(0) 要移动剩余引用,不适合反复出队。deque 支持两端快速加入和移除,中间随机索引会逐渐变慢。
| 任务 | 合适起点 | 需要注意 |
|---|---|---|
| 按下标反复读写 | list | 中间插入和删除要搬动元素 |
| 后进先出 | list 的 append/pop | 弹出空栈需要约定 |
| 先进先出 | deque 的 append/popleft | 不用 list.pop(0) 模拟长队列 |
| 持有节点并频繁接链 | 链式结构 | 查找节点仍要遍历,并有指针与分配开销 |
已知链表节点时,重新连接相邻引用可以是 O(1);如果先按值找节点,整个删除仍可能 O(n)。链表也不能自动解决缓存局部性和内存开销问题。在 Python 中自己造链表主要用于理解结构,日常代码先检查标准容器能否表达需求。
用栈验证嵌套
括号是否合法取决于最近尚未匹配的左括号。扫描到左括号就压栈,扫描到右括号就与栈顶匹配。扫描前缀后的不变量是:栈从底到顶按出现顺序保存仍待闭合的左括号。普通字符忽略,所以它只是括号结构检查器,不是完整的代码语法分析器。
def brackets_error(text):
pairs = {')': '(', ']': '[', '}': '{'}
stack = []
for index, char in enumerate(text):
if char in '([{':
stack.append((char, index))
elif char in pairs:
if not stack or stack[-1][0] != pairs[char]:
return index
stack.pop()
return stack[-1][1] if stack else None
if __name__ == '__main__':
print(brackets_error('a[({x})]'))
print(brackets_error('([)]'))
([)] 的左右括号总数相同,嵌套仍错误;只比较计数会漏掉这个反例。遇到第一个不匹配的右括号返回其下标;扫描结束仍有左括号时返回最后一个未闭合位置。最多每个字符入栈、出栈各一次,因此时间 O(n),最坏额外空间 O(n)。
引用与修改也是边界
把同一个列表放入多个位置会共享引用:rows=[[]]*3 的三行不是独立列表。需要独立状态时用列表推导分别创建。遍历时删除当前列表元素可能跳过下一项,批量过滤可以建立新列表,或明确使用下标与修改规则。
练习与面试自查
把检查器改为同时返回所有未匹配位置,保持报告顺序确定。说明字符串中的引号、转义和注释为什么需要额外词法状态,不能仅靠当前栈解决。用很深的括号串验证实现没有依赖递归栈。
面试中应能回答:双端队列为何适合BFS?因为发现的节点按距离层次排队,队首移除无须搬动整个数组。链表删除为何未必比数组快?查找、分配和局部性都是总成本的一部分。
本地运行
示例仅使用 Python 标准库,适用于 Python 3.10 及以上。把 example.py 与 test_example.py 保存到同一个目录,在该目录运行:
python example.py
python -m unittest -v test_example.py
测试检查本文列出的边界;新增练习仍需自己补充断言。