CMU 15-445 Fall 2022 Project0: C++ Primer
Lapin Gris Lv3

这个项目是使用 Modern C++ 17 实现一个 字典树(Trie
项目难点在于理解项目里如何使用 TrieNode 和 TrieTerminalNode 来组合实现一个 Trie Tree。另外一个难点是对 unique_ptr 的理解。理解 unique_ptr 智能指针对生命周期的管理。

背景

项目提供了图对 Trie Tree 介绍的,理解这两张图,基本理解了这个项目里我们需要实现什么了。
下面这张图是我们最终实现的 Trie Tree。图中红色的格子是要实现的 Terminal Node(TrieNodeWithValue),
图中包含字母的格子是要实现的 Trie Node。其中 Terminal Node 算是 Trie Node 的一个特殊情况。

回来继续介绍 TrieNode,他有三个成员

char key_char_;
bool is_end_{false};
std::unordered_map<char, std::unique_ptr<TrieNode>> children_;

key_char_ 是路径的字符,比如 H,A,E
is_end_ 是表示当前 Node 是否是 Terminal Node,默认初始化为 false
children_ 是一个 hashmap,key 是当前节点的 key_char_ ,value 是一个指向下一个节点的独占指针 unique_ptr 。图中代表 children_ 是 T 和 V 的那一部分。T 和 V 都是 A 节点的子节点。
image.png

为什么说 Terminal Node 是 Trie Node 的特殊情况,因为项目中TrieNodeWithValue 是从 TrieNode 继承过来的,算是子类了。
比如,我们现在要用 Trie Tree 存储N个键值对,其中两个键值对是 (ab, 1) (ac, “val”) 。那么他们存放在书上就是下图展示的样子,因为我们要实现的 TrieNodeWithValue 的 value 被设计成支持任意类型的 value 存储,所以 TrieNodeWithValue 是一个模板类。
image.png

实现 TrieNode

InsertChildNode & GetChildNode & RemoveChildNode

实现 ChildNode 相关操作就是玩 unordered_map 。C++ 里面是 unordered_map 就是通常所说的 hashmap。
image.png
image.png
image.png

实现 TrieNodeWithValue

构造函数

第一次碰到这个函数,有一点懵,没明白子类怎么移动构造,现在想想其实很简单,利用初始化列表将父类 move,然后在大括号里最定制化修改。果然, C++ 好久不用就容易忘。
image.png

实现 Trie

构造函数

我们 root_ Node 是一个空节点,而且要是一个由 unique_ptr 管理的 TrieNode。最初时候没想到使用 make_unique 来构造一个,想了挺久的(蠢哭了……)
image.png

Insert

项目要求要支持并发访问,加锁是必须的了。当然在实现的视线可以先实现不加锁版本的 Insert 保证代码没有逻辑错误,然后再为代码加锁。
其实我的最初版本,我的逻辑代码和加锁是混合在一起的,各种 for loop 中间 return,有了 return 就需要在 return 之前把之前加的锁释放掉。。。就挺混乱的,另外因为多个 return 位置,容易忘记解锁。
后来,我参考内核调度器那块代码,把解锁放到函数末尾做,当代码中需要 return 的时候,用 goto 跳转到 out label,这样也就统一了解锁的位置,代码看起来舒服多了。
其实更好的办法是像 DPDK 那样实现一个 unsafe 版本的函数,在实现一个 safe 版本的,锁加在 safe 版本的函数里,在 safe 调用 unsafe 函数,这样实现了逻辑代码和加锁代码分离。唯一的缺点可能是锁的粒度大,不容易优化锁的粒度吧。

首先从 root_ 的指针,开始遍历 Tree,执行插入操作
我们选择是遍历整个 string,我们把 string 分成两部分,[0, key.size()-1) 和 [key.size()-1] 两部分来实现。这样写的代码更加简单,感觉项目里的注释描述稍微有点引导学生将字符串分成两部分处理。其实完全没有必要,这样作为一个整体处理逻辑也更简单。
遍历整个 string 会把整个路径所有节点类型设置为 TrieNode,所以最后需要将 Terminal Node move 一下,转为 TrieNodeWithValue 类型。然后让 unique_ptr 指针指向 TrieNodeWithValue即可。
image.png

Remove

删除要求如果需要中间节点没有 child 节点,这个节点也需要被 remove 掉。这就说明在遍历的时候我们要记录父节点的指针,但是指针一旦调到下一个节点,就回不去了,我们用的也不是双向版本的指针。所有,我们要记录我们的遍历的路径,我们用 vector 存,其实用 stack 也没问题。
最终,我们先遍历一遍 string,走到底部。然后开始反向遍历,检查节点有没有子节点,没有就将节点删除。
image.png

GetValue

root_ 开始遍历,项目要求检查存储在 Terminal Node 的 value 类型。这里借助 dynamic_cast 来判断。
image.png

Gradescope

image.png