Ran Wei/计算机科学系列/06
English
计算机科学基础 — Ran Wei

模块 06: 核心数据结构

按所需操作选择表示:数组、链表、栈、队列与哈希表。

约 5 小时4 个时段2 个实验8 道练习6 道自测题

完成后你能够

  • 比较索引与链接存储。
  • 实现链式栈。
  • 使用先进先出队列。
  • 解决哈希碰撞。
  • 解释摊还成本。

开始之前

建议先修模块: 03, 05.

了解引用与操作计数。实验介绍类:self 表示当前实例,属性保存状态。

目录

学习计划

5 小时

四个 75 分钟时段,包含练习。拓展任务或不熟悉的先修知识可能需要更多时间。进度本地保存,两种语言共享。

时段 275 分钟
构建与探究
时段 375 分钟
应用与拓展
时段 475 分钟
推理与复习
1

抽象数据类型与表示

抽象数据类型规定操作与行为,数据结构实现它们。栈承诺后进先出,无论用数组还是链接。需区分操作约定与成本,按索引读取、头部删除、键查询或顺序扫描等工作负载选择,还要规定空情况、重复策略与可变值所有权。

检查理解

FIFO 是表示还是行为规则?

完整解答

行为规则,多种表示可实现。

2

数组与动态数组

数组把元素放入索引槽位,在通常 RAM 模型中支持常数索引访问。动态数组容量耗尽时扩容,单次追加可能复制 n 项,但几何增长让多次追加总复制线性,因此摊还 O(1)。头部插入需移动 O(n) 项。Python 列表保存对象引用,而非直接连续存放任意对象。局部性也影响实际扫描效率。

检查理解

摊还 O(1) 意味着每次追加都是 O(1) 吗?

完整解答

不是,偶尔扩容可为 O(n)。

3

链表与局部更新

链节点保存值和下一节点引用,头部插入只改变少量引用。访问索引 i 要沿链前进,成本 O(i)。已知节点后插入 O(1),寻找该节点却可能 O(n)。单链表删除通常需前驱。链接增加内存且可能降低局部性;栈实验展示这些引用。

检查理解

寻找并删除任意链表项总是 O(1) 吗?

完整解答

不是,定位项目与前驱可能需要扫描。

4

栈与队列

栈先删除最新项,队列先删除最早项。撤销记录用栈,等待任务常用队列。链式栈在头部 O(1) 推入弹出。deque 支持两端高效操作,避免 list.pop(0) 移动引用。空删除需规定结果或异常。递归使用调用栈,广度搜索使用队列,行为顺序决定算法。

检查理解

哪种结构按到达顺序处理请求?

完整解答

先进先出队列。

5

哈希表、碰撞与负载

哈希函数把键映射到桶,不同键可碰撞,因此需在链中比较原键或采用其他策略。更新现有键应替换值而非重复插入。期望 O(1) 依赖合适哈希与受控负载,最坏链长可 O(n)。负载因子为项目数除桶数,必要时扩容重哈希。实验字节和哈希故意很差,变位词碰撞;Python 字典实现更复杂。

检查理解

相同哈希意味着相同键吗?

完整解答

不意味着,需比较键区分碰撞。

6

常见误解

  • 只数引用修改会遗漏定位节点的搜索。
  • 哈希不是唯一身份。
7

实验准备

下载脚本,在终端中使用 Python 3.11 或更新版本运行:python m06_structures.py. Windows 也可使用 py -3;部分系统使用 python3。实验仅用标准库。先预测结果,再运行并完成变体。不要使用 -O,以保留断言。下方输出由构建器实际运行捕获,两种语言使用相同代码与输出。

8

实验 1 — 链式栈与队列

跟踪 head、next 引用,比较相同输入的删除顺序。

下载 m06_structures.py

"""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
  1. 增加不删除的 peek。
  2. 测试单次推入弹出与空队列。
  3. 画出两次推入后的链。
完整解答

peek 检查非空后返回 head.value,不更新 head。推入 A、B 后为 B→A→None,空 deque.popleft 抛 IndexError。

9

实验 2 — 碰撞处理

ab 与 ba 同桶但键不同,更新 ab 不增加链长度。

下载 m06_hash.py

"""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
  1. 仅用一桶,统计扫描项。
  2. 增加删除并规定缺失键行为。
  3. 桶数翻倍,重新插入所有项。
完整解答

一桶产生线性链。删除需找键并移除,不存在则抛 KeyError。扩容后重新计算每个键的桶,不能照搬旧链。

10

练习与完整解答

先尝试,再展开解答。★ 应用概念;★★ 结合概念;★★★ 进行设计或证明。

练习 1 — 后进先出★

推入 A、B、C,弹出两次后剩什么?

完整解答

先 C 后 B,剩 A。

练习 2 — 先进先出★

入队 A、B、C,出队两次。

完整解答

移除 A、B,剩 C。

练习 3 — 索引成本★★

比较数组与单链表读取索引 500。

完整解答

数组 O(1),链表约五百次链接遍历,一般最坏 O(n)。

练习 4 — 负载因子★★

十八项八桶,负载多少?

完整解答

平均 2.25 项每桶,实际链可差很多。

练习 5 — 碰撞证明★★

所有字符串能唯一映射到 256 桶吗?

完整解答

不能,超过 256 个字符串由鸽巢原理必碰撞。

练习 6 — 摊还复制★★★

容量翻倍,到十六前总复制多少?

完整解答

1+2+4+8=15,几何和小于最终翻倍容量,说明扩容总工作线性。

练习 7 — 选择结构★★★

不可变书 ID 重复查询、不要求有序,选什么?

完整解答

哈希映射期望 O(1),需规定缺失键与唯一性;若需要有序范围查询应重新考虑。

练习 8 — 已知节点★★

已知节点后 O(1) 插入为何不代表按书名插入 O(1)?

完整解答

书名查询先需定位节点,可能扫描全链。

11

自测

选择答案查看反馈,重置后可重做。无需 JavaScript 也可阅读答案表。

1

栈是什么?

2

list.pop(0) 通常成本?

3

碰撞意味着什么?

4

负载因子是什么?

5

链表随机访问通常如何?

6

期望常数查询需要什么?

答案表
  1. A — 最新项先离开。
  2. B — 剩余引用需移动。
  3. C — 处理碰撞可保留双方。
  4. A — 它概括占用。
  5. B — 需沿 next 前进。
  6. C — 最坏仍可线性。
12

引导阅读

13

复习与下一步

为每种结构说明适用负载,跟踪栈引用与碰撞更新。模块 07 将用栈和队列遍历递归结构。

14

关键术语

术语含义
抽象数据类型操作与行为约定。
摊还成本对具有最坏总成本界的序列分摊单次成本。
碰撞不同键取得同一桶索引。