题目来源:改编自 LeetCode 705. 设计哈希集合。本 Lab 使用课程自定义输入输出协议和独立测试,不复制来源站点的代码或测试。
学习目标
前置知识与环境
先阅读第 9.2 节 散列表的"链地址法"小节,并回看"装填因子"一节——链地址法允许装填因子超过 1,冲突全部表现为链表长度。
本题桶数
在 Lab 目录执行 make doctor 检查环境;未安装 Make 时可在仓库根使用 pnpm 兜底命令(见下文"运行")。
任务
设计一个哈希集合,支持三种操作:
add x:把x加入集合;x已存在时无任何效果;contains x:x在集合中输出1,否则输出0;remove x:把x从集合中删除;x不存在时无任何效果。
实现口径已钉死:链地址法。桶数组共
输入格式
- 第一行:两个整数
B和n,分别为桶数与操作数; - 接下来
n行,每行一条指令,格式为add x、contains x或remove x。
输出格式
对每条 contains 指令,输出一行一个整数 1 或 0。
数据范围与限制
| 项目 | 范围 |
|---|---|
桶数 B | 3 ≤ B ≤ 97(故意压小,制造大量冲突) |
操作数 n | 1 ≤ n ≤ 2×10⁵ |
元素 x | 0 ≤ x ≤ 10⁶ |
| 散列函数 | |
| 时间复杂度要求 | 单次操作平均与链长成正比 |
正常、边界与错误情况
- 正常:
add后contains输出1;remove后contains输出0; - 集合语义:对同一值
add两次,集合中只有一份,remove一次后contains即为0; - 边界:对空集合
contains与remove;删除后再add同一值应重新可查到;x = 0与x = 10⁶两个端点值;B = 3时几乎所有元素挤在同一条链上; - 删除陷阱:摘除头节点时要同步更新桶的头指针,漏更新会出现"野指针"或死循环。
样例
样例 1
input
7 12
add 1
add 2
add 1
contains 1
contains 3
remove 2
contains 2
remove 5
add 1000000
contains 1000000
remove 1
contains 1output
1
0
0
1
0第二次 add 1 是重复插入,无效果,故 contains 1 输出 1;3 从未加入,输出 0;remove 2 后 contains 2 变为 0;remove 5 删除不存在的元素,无效果;1000000 加入后可查到;remove 1 之后 contains 1 归零。
样例 2
input
3 6
add 0
add 3
add 6
contains 3
remove 3
contains 3output
1
0remove 3 摘掉的是链的中间节点,摘除后其余元素不受影响。
实现提示
- 节点结构:
struct Node { long long key; Node* next; },桶数组std::vector<Node*> buckets(B, nullptr); add(x):先沿链查重(已存在直接返回),再头插新节点并更新桶头指针;remove(x):沿链记录prev,命中后prev->next = cur->next;prev为空说明删的是头节点,要同步把桶头指针改成cur->next;- 动态
new出的节点在删除时delete,程序结束前逐链释放(课程不强制,但写上是好习惯); - 想一想:为什么本题不需要 Lab 09-E-05 里的墓碑?链地址的删除只动一条链,不会截断别的元素的查找路径。
运行
powershell
make run
# 免 Make 兜底:在仓库根执行
pnpm lab:run -- labs/chapter-09/exercise/E-09-06-design-hash-set-chaining需要单用例或严格评分时使用 make run CASE=006-stress-2e5 与 make score;也可用 pnpm lab:score -- labs/chapter-09/exercise/E-09-06-design-hash-set-chaining 兜底。
完成清单
思考与复盘
、集合中有 个元素时平均链长与装填因子各是多少?链地址法允许装填因子超过 1 的原因是什么?- 对照 Lab 09-E-05:为什么链地址删除不需要墓碑?如果本题改用开放定址,压小的
会带来什么后果? - 头插法把新节点插在链头,最常被查到的元素却不一定在链头。若想在频繁
contains的负载下缩短平均查找长度,可以做什么改进?代价是什么?