抽象数据类型与表示
抽象数据类型规定操作与行为,数据结构实现它们。栈承诺后进先出,无论用数组还是链接。需区分操作约定与成本,按索引读取、头部删除、键查询或顺序扫描等工作负载选择,还要规定空情况、重复策略与可变值所有权。
FIFO 是表示还是行为规则?
完整解答
行为规则,多种表示可实现。
数组与动态数组
数组把元素放入索引槽位,在通常 RAM 模型中支持常数索引访问。动态数组容量耗尽时扩容,单次追加可能复制 n 项,但几何增长让多次追加总复制线性,因此摊还 O(1)。头部插入需移动 O(n) 项。Python 列表保存对象引用,而非直接连续存放任意对象。局部性也影响实际扫描效率。
摊还 O(1) 意味着每次追加都是 O(1) 吗?
完整解答
不是,偶尔扩容可为 O(n)。
链表与局部更新
链节点保存值和下一节点引用,头部插入只改变少量引用。访问索引 i 要沿链前进,成本 O(i)。已知节点后插入 O(1),寻找该节点却可能 O(n)。单链表删除通常需前驱。链接增加内存且可能降低局部性;栈实验展示这些引用。
寻找并删除任意链表项总是 O(1) 吗?
完整解答
不是,定位项目与前驱可能需要扫描。
栈与队列
栈先删除最新项,队列先删除最早项。撤销记录用栈,等待任务常用队列。链式栈在头部 O(1) 推入弹出。deque 支持两端高效操作,避免 list.pop(0) 移动引用。空删除需规定结果或异常。递归使用调用栈,广度搜索使用队列,行为顺序决定算法。
哪种结构按到达顺序处理请求?
完整解答
先进先出队列。
哈希表、碰撞与负载
哈希函数把键映射到桶,不同键可碰撞,因此需在链中比较原键或采用其他策略。更新现有键应替换值而非重复插入。期望 O(1) 依赖合适哈希与受控负载,最坏链长可 O(n)。负载因子为项目数除桶数,必要时扩容重哈希。实验字节和哈希故意很差,变位词碰撞;Python 字典实现更复杂。
相同哈希意味着相同键吗?
完整解答
不意味着,需比较键区分碰撞。
常见误解
- 只数引用修改会遗漏定位节点的搜索。
- 哈希不是唯一身份。
实验准备
下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m06_structures.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。
实验 1 — 链式栈与队列
跟踪 head、next 引用,比较相同输入的删除顺序。
"""A linked stack and a FIFO queue with explicit empty behaviour."""
from collections import deque
class Node:
def __init__(self, value, next_node=None):
self.value, self.next = value, next_node
class Stack:
def __init__(self):
self.head = None
def push(self, value):
self.head = Node(value, self.head)
def pop(self):
if self.head is None:
raise IndexError("empty stack")
node = self.head
self.head = node.next
return node.value
stack, queue = Stack(), deque()
for book in ["Dune", "Foundation", "Solaris"]:
stack.push(book); queue.append(book)
print("stack:", [stack.pop() for _ in range(3)])
print("queue:", [queue.popleft() for _ in range(3)])
try:
stack.pop()
except IndexError:
print("empty pop rejected")
stack: ['Solaris', 'Foundation', 'Dune']
queue: ['Dune', 'Foundation', 'Solaris']
empty pop rejected
- 增加不删除的 peek。
- 测试单次推入弹出与空队列。
- 画出两次推入后的链。
完整解答
peek 检查非空后返回 head.value,不更新 head。推入 A、B 后为 B→A→None,空 deque.popleft 抛 IndexError。
实验 2 — 碰撞处理
ab 与 ba 同桶但键不同,更新 ab 不增加链长度。
"""A deliberately small chained table makes collisions visible."""
class Table:
def __init__(self, buckets=4):
if buckets < 1:
raise ValueError("positive bucket count required")
self.buckets = [[] for _ in range(buckets)]
def slot(self, key):
return sum(key.encode("utf-8")) % len(self.buckets)
def put(self, key, value):
chain = self.buckets[self.slot(key)]
for i, (stored, _) in enumerate(chain):
if stored == key:
chain[i] = (key, value); return
chain.append((key, value))
def get(self, key):
for stored, value in self.buckets[self.slot(key)]:
if stored == key:
return value
raise KeyError(key)
table = Table()
for key, value in [("ab", 1), ("ba", 2), ("c", 3), ("ab", 4)]:
table.put(key, value)
print("chains:", table.buckets)
assert table.get("ab") == 4 and table.get("ba") == 2
try:
table.get("missing")
except KeyError:
print("missing key rejected")
chains: [[], [], [], [('ab', 4), ('ba', 2), ('c', 3)]]
missing key rejected
- 仅用一桶,统计扫描项。
- 增加删除并规定缺失键行为。
- 桶数翻倍,重新插入所有项。
完整解答
一桶产生线性链。删除需找键并移除,不存在则抛 KeyError。扩容后重新计算每个键的桶,不能照搬旧链。
练习与完整解答
先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。
推入 A、B、C,弹出两次后剩什么?
完整解答
先 C 后 B,剩 A。
入队 A、B、C,出队两次。
完整解答
移除 A、B,剩 C。
比较数组与单链表读取索引 500。
完整解答
数组 O(1),链表约五百次链接遍历,一般最坏 O(n)。
十八项八桶,负载多少?
完整解答
平均 2.25 项每桶,实际链可差很多。
所有字符串能唯一映射到 256 桶吗?
完整解答
不能,超过 256 个字符串由鸽巢原理必碰撞。
容量翻倍,到十六前总复制多少?
完整解答
1+2+4+8=15,几何和小于最终翻倍容量,说明扩容总工作线性。
不可变书 ID 重复查询、不要求有序,选什么?
完整解答
哈希映射期望 O(1),需规定缺失键与唯一性;若需要有序范围查询应重新考虑。
已知节点后 O(1) 插入为何不代表按书名插入 O(1)?
完整解答
书名查询先需定位节点,可能扫描全链。
自测
选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。
栈是什么?
list.pop(0) 通常成本?
碰撞意味着什么?
负载因子是什么?
链表随机访问通常如何?
期望常数查询需要什么?
答案表
- A — 最新项先离开。
- B — 剩余引用需移动。
- C — 处理碰撞可保留双方。
- A — 它概括占用。
- B — 需沿 next 前进。
- C — 最坏仍可线性。
引导阅读
- Python deque — 比较两端操作与列表头部删除。
复习与下一步
为每种结构说明适用负载,跟踪栈引用与碰撞更新。模块 07 将用栈和队列遍历递归结构。
关键术语
| 术语 | 含义 |
|---|---|
| 抽象数据类型 | 操作与行为约定。 |
| 摊还成本 | 对具有最坏总成本界的序列分摊单次成本。 |
| 碰撞 | 不同键取得同一桶索引。 |