Task 2.1 — The Book Structure
匿名同学 笔记(稍有难度,建议先画图手动推演再写代码)
题目理解
📌 简介
ProcessingBook类似哈希表。
- 键(key):
Transaction对象(已签名)
- 值(value):交易金额
- 每笔交易必须唯一存储,不能重复设定不同金额。
🧠 结构核心思想
🔢 签名(signature)说明
- 签名是字符串,由小写字母(a–z)和数字(0–9)组成。
- 每个字符有 36 种可能 → 账本有 36 个页面(pages)
- 页面索引顺序:a, b, ..., z, 0, 1, ..., 9
✅ 插入规则
- 使用签名第
i位字符决定要插入的页。
- 若该页为空 → 存入金额
- 若该页已被占用:
- 若为另一个
Transaction→ 冲突: - 创建子账本(ProcessingBook)
- 将冲突交易和新交易都转移进去
- 在子账本中使用下一位字符继续处理(递归)
- 若为子账本 → 递归调用
__setitem__
❌ 禁止行为
- 若同一签名的交易已经存过一个值,不能设定不同的值。
- 若值相同,可重复设置,不报错。
- 读取不存在的交易 → 抛出
KeyError
🧩 你需要实现的方法
✅ __setitem__(transaction, amount)
- 根据签名字符递归插入交易。
- 若已存在但金额不同,记录错误。
✅ __getitem__(transaction)
- 根据签名字符递归查找。
- 若找不到 → 抛出
KeyError
✅ get_error_count()
- 返回错误设置次数(即试图更新已有交易为不同金额的次数)。
⚠️ 约定和注意事项
- 所有交易对象都有有效签名(你不需要手动签名)。
- 所有交易签名长度一致。
- 金额都是正整数。
- 不允许假设签名长度最大值(和 Task 1 要分开考虑)。
- 只能在
ProcessingBook中存储一次交易金额。
🔁 冲突处理机制图解说明
场景 | 处理方式 |
两个签名首字符相同 | 在该页创建新 ProcessingBook |
子账本中再次冲突 | 再次创建新 ProcessingBook |
一直到字符不同为止 | 成功分配到不同页 |
题解思路
1. 核心目标
- 将
Transaction对象存储在递归哈希结构中。
- 根据 交易签名 (signature) 的每个字符逐层分配到
pages(类似字典树 / trie 的思想)。
- 支持插入 (
__setitem__) 和查找 (__getitem__),并在发生冲突时递归建立子表。
- 检测错误:如果同一个签名对应的
amount不一致,则记录在_error_count中。
2. 数据结构设计
- ProcessingBook
pages:长度固定的ArrayR,大小为 36(a–z, 0–9)。_error_count:记录错误次数。
- Somename(嵌套类)
someothername:一个长度为 2 的数组,存储Transaction和amount。sub_book:递归子表,发生冲突时创建。contain_sub_book():判断是否存在子表。
3. 插入逻辑 (__setitem__)
- 定位字符位置
- 根据
transaction.signature[index]找到对应的下标位置。
- 情况 A:该位置为空
- 直接存放新的
Somename(transaction, amount)。
- 情况 B:该位置已有内容
- 如果已有 子表 → 进入子表递归插入。
- 如果没有子表:
- 签名相同但金额不同 → 计入
_error_count。 - 签名不同 → 创建子表,存放已有交易和新交易。
- 递归进行,直到找到合适位置。
4. 查找逻辑 (__getitem__)
- 从签名第一个字符开始查找
pages对应位置。
- 如果存在子表 → 递归进入下一层。
- 如果是
Somename且签名完全匹配 → 返回对应的金额。
- 如果不存在或不匹配 → 抛出
KeyError。
难点:1. 子表处理 2. 数据结构连贯性
Jason 笔记
查看思路
- 账本类似哈希表,有 36 个「页面」(a–z, 0–9)。
- 每个
Transaction的signature决定它存放在哪一页: - 第 1 个字符 → 顶层页面。
- 如果冲突(已有不同交易占位) → 创建子 ProcessingBook,在里面用第 2 个字符作为索引。
- 再冲突 → 继续递归到第 3 个字符 … 直到分开。
- 不能直接更新交易金额:
- 如果同一签名再插入相同金额 → 安全忽略。
- 如果金额不同 → 错误,记录一次 error。
核心思路
- 用递归结构解决冲突:每一层账本只看签名的一个字符。
- 插入时逐层进入,直到找到空位或必须新建子账本。
- 查找时逐层进入,直到找到交易或确定不存在。
- 维护一个全局
errors计数器,每次试图「修改金额」时自增。
伪代码
- 插入
__setitem__ - 输入:交易对象,金额 amount。
- 找到签名第
depth个字符,计算页索引。 - 如果该页是空:放进去
(交易对象, amount)。 - 如果该页已有一个
(交易对象2, amount2): - 如果
交易对象2.signature == tx.signature: - 如果金额相同 → 忽略。
- 如果金额不同 → errors += 1,不更新。
- 否则:创建子 ProcessingBook,递归存储
交易对象2和交易对象。 - 如果该页已有一个子 ProcessingBook:递归插入。
- 查找
__getitem__ - 输入:交易对象 tx。
- 找到签名第
depth个字符,计算页索引。 - 如果该页是空 → KeyError。
- 如果是
(交易对象2, amount2)且签名匹配 → 返回 amount2。 - 如果是 ProcessingBook → 递归继续。
- 否则 KeyError。
- 错误计数 (get_error_count)
- 返回
self.errors。
Task 2.2 — Deleting
匿名同学笔记(稍有难度,建议先画图手动推演再写代码)
题目理解
📌 题目目标:
我们需要为
ProcessingBook 类实现一个删除功能 __delitem__,它可以:- 从嵌套的哈希结构中删除一个指定的交易(Transaction);
- 如果删除某个交易后,它所在的子书(sub book)中只剩下一个交易:
- 那么该子书应当折叠回上一层,这个剩下的交易直接上浮,放回其父级页面中;
- 删除一个不存在的交易时,应抛出
KeyError;
- 整体逻辑为对 Task 2.1(添加时多层嵌套)的一种“反向处理”。
🔍 结构说明:
ProcessingBook的pages是一个ArrayR,其中的每个位置可以包含:- 一个
TransactionWrapper,其中是具体交易; - 或者一个嵌套的
sub_book(下一层 ProcessingBook);
- 因此,删除操作也要沿着
Transaction.signature的字符路径,逐层进入正确的子书,直到找到目标交易;
- 删除之后,若某一层子书只剩一个交易(其余为
None),就要将这个交易上浮,折叠掉该子书
解题思路
🧠 实现思路概述:
- 从顶层出发,递归向下查找目标交易的位置:
- 根据交易的
signature逐字符定位; - 每一层的字符决定进入哪个 page;
- 若 page 中是子书,则继续进入下一层;
- 若 page 中是交易,检查是否为目标交易。
- 在进入每一层之前,判断是否存在可折叠的情况:
- 使用辅助函数判断:当前页面中是否除了目标位置以外还有其他内容(交易或子书);
- 如果没有,说明当前层结构已无必要存在,可标记为
need_collaps = True; - 同时记录当前层数,用于后续 collapse 操作。
- 找到目标交易后,执行删除操作:
- 若不需要折叠,则直接将交易设为
None; - 若需要折叠,则将整个 page 设为
None,并调用collaps函数向上折叠结构。
- 折叠操作
collaps的作用: - 定位到需要折叠的那一层的
ProcessingBook对象; - 将其子书中唯一剩下的内容复制回当前页面中;
- 相当于将嵌套层级减少一级,使结构更加紧凑。
- 异常处理:
- 如果在任何层找不到目标交易,或路径上存在空位,应抛出
KeyError
Jason 笔记
查看思路
题目要求
- 需要支持
del book[transaction]删除交易。
- 删除后要检查:
- 如果子 ProcessingBook 变空 → 删除该子簿,父页设为 None。
- 如果子 ProcessingBook 只剩 1 个交易 → 折叠,把这个唯一的交易移回父级。
- 如果交易不存在 → 抛
KeyError。
- 删除时要维护全局
__length计数器,保证__len__O(1)。
核心思路
- 删除和插入的逻辑正好相反:
- 插入时:冲突 → 建子簿。
- 删除时:冲突解除 → 折叠子簿。
- 在每次删除时:
- 如果删除成功,
__length -= 1。 - 如果删除导致子簿结构发生变化,要更新父级的页面指向。
伪代码
- 删除
__delitem__ - 输入:交易对象 tx。
- 找到签名第
depth个字符 → 页索引。 - 如果该页为空 → KeyError。
- 如果该页是
(交易对象2, amount2): - 如果签名相同 → 删除,父页设为 None,
__length -= 1。 - 否则 → KeyError。
- 如果该页是子 ProcessingBook:
- 递归删除。
- 删除后检查子簿:
- 如果子簿空 → 父页设为 None。
- 如果子簿只剩 1 个交易 → 折叠,把唯一交易移回父级页。
- 折叠逻辑
- 遍历子簿的 pages,找到唯一非空元素,直接提升到父级。
Task 2.3 — Iterating
Jason 笔记 🅰️ 版 → 依靠数据结构 #推荐
查看思路
题目要求
- 需要支持
len(book)返回账本中所有交易数量。
- 需要支持
for tx, amt in book:遍历所有交易。
- 遍历时的顺序必须按照
LEGAL_CHARACTERS的顺序(a–z → 0–9)。
- 遍历结果是一个个二元组
(transaction, amount)。
- 假设遍历过程中不会有新增/删除操作。
核心思路
__len__实现- 必须是 O(1),不能在调用时去递归数。
- 在插入时成功添加新交易 →
__length += 1。 - 在删除时成功删除交易 →
__length -= 1。 - 所以
__len__只需返回self.__length。
- 迭代器实现
__iter__:初始化一个栈或游标,返回迭代器对象。__next__:使用深度优先遍历 (DFS) 的方式:- 取出当前
(book, page_index)。 - 如果是
None→ 跳过。 - 如果是
(transaction, amount)→ 返回该二元组。 - 如果是子 ProcessingBook → 把它和索引放入栈,递归进入。
- 每次只处理一个元素,所以
__next__是 O(1)。 - 直到栈空,抛
StopIteration。
伪代码
迭代初始化
__iter__- 创建一个栈(或等价结构),保存「当前账本 + 页索引」。
- 初始时,将
(self, 0)压入栈。
- 返回账本对象自身作为迭代器。
迭代步骤
__next__- 当栈非空:
- 弹出栈顶
(当前账本, 当前索引)。 - 如果索引超出 36 → 跳过。
- 否则,把
(当前账本, 索引+1)压回栈。 - 查看该页:
- 空 → 跳过。
- 若为单个交易:返回
(交易, 金额)。 - 若为子账本:把
(子账本, 0)压入栈,继续循环。
- 当栈为空:抛出
StopIteration。
Jason 笔记 🅱️ 版 → 依靠递归
查看思路
题目要求
- 需要支持
len(book)返回账本中所有交易数量。
- 需要支持
for tx, amt in book:遍历所有交易。
- 遍历时的顺序必须按照
LEGAL_CHARACTERS的顺序(a–z → 0–9)。
- 遍历结果是一个个二元组
(transaction, amount)。
- 假设遍历过程中不会有新增/删除操作。
核心思路
- 索引跟踪:用一个索引变量
track_index,从 0 遍历到 35,对应合法字符顺序。
- 递归展开:当某一页是子账本时,不一次性取出所有元素,而是把子账本转成迭代器,逐步交给它去产出元素。
- 状态保存:用一个变量
last_element来记录当前是否正在遍历某个子账本: - 如果
last_element还没结束 → 优先继续从里面取元素。 - 如果子账本遍历结束 → 重置
last_element = None,然后继续向下扫描父账本的下一页。
- 终止条件:当索引扫描到最后(≥36)且没有子账本 → 抛出
StopIteration。
伪代码
迭代初始化(
__iter__)- 设置一个索引变量
track_index = 0。
- 设置一个变量
last_element = None,表示当前是否正在遍历子账本。
- 返回当前对象作为迭代器。
迭代步骤(
__next__)- 检查是否在遍历子账本
- 如果
last_element是一个迭代器: - 尝试从中获取下一个元素。
- 如果成功 → 返回这个元素。
- 如果子账本遍历结束 → 把
last_element设为None,并让索引+1。
- 循环检查当前账本的页面
- 当
track_index小于 36(合法字符总数): - 取出当前页面。
- 如果页面是一个 单个交易:
- 索引
+1。 - 返回该交易。
- 如果页面是一个 子账本:
- 把
last_element设置为该子账本的迭代器。 - 从子账本迭代器获取一个元素并返回。
- 如果页面为空:
- 索引
+1,继续循环。
- 结束条件
- 如果所有页面都遍历完 (
track_index >= 36) → 抛出StopIteration。