# Project #0 - C++ Primer
bustub 项目用 C++ 17 编写,但 C++ 11 已经够用。
C++ 相关教程:
关于GDB调试:
- Debugging Under Unix: gdb Tutorial
- GDB Tutorial: Advanced Debugging Tips For C/C++ Programmers
- Give me 15 minutes & I'll change your view of GDB [VIDEO]
git 命令:
PROJECT SPECIFICATION
该项目中,实现一个由并发 Trie 支持的kv存储。Trie 是一种有效的 ordered-tree 数据结构,用于检索给定键的值。为了简化解释,我们假设键都是非空的可变长度字符串,但实际上它们可以是任意类型。
Trie 中的每个节点存储一个键的单个字符,并且可以有多个子节点,这些子节点表示不同的可能的下一个字符。当到达一个键的结尾时,将设置一个标志来指示其对应的节点是一个结束节点。
在下面的示例中,trie 存储键 HELLO
、 HAT
和 HAVE
。

你将实现的kv存储能存储 map 到任何类型的 value 的 string key。key 对应的 value 值存储在表示该 key 的最后一个字符的结点(又称 terminal node)中。
例如,考虑在 trie 中插入 kv 对(“ ab”,1)和(“ ac”,“ val”)。这两个键共享同一个父节点 “a”
。在左边的子节点中,与键 “ab”
对应的值 1
存储在节点 “b”
中,与键 “ac”
对应的值 “val”
存储在节点 “c”
中。

IMPLEMENTATION
您只需要修改 BusTub 中的一个文件 p0_ trie.h
(src/include/primer/p0 _ trie.h)。除了测试代码之外,您不需要修改存储库中的任何其他文件。
函数原型和成员变量在文件中指定。该项目要求您填写所有构造函数、析构函数和成员函数的实现。如果您认为合适,可以添加任何其他的辅助函数和成员变量,但不要修改现有的函数和变量。
Task #1 - Templated Trie
在这个头文件中,我们定义了三个必须实现的类。我们建议您首先实现 Trie 类的单线程版本,然后再转向并发版本。
TrieNode Class
TrieNode
类在 Trie 中定义单个节点。TrieNode
保存 key 的一个字符,is_end
flag 指示它是否标记 key string 的结束。child nodes 的 key 字符从成员变量 children_
这个map 访问得到,它存储char
到 unique_ptr<TrieNode>
的映射。
注意:child node 为 unique_ptr
,因此将它们分配给其他变量时需要小心。InsertChildNode
和 GetChildNode
都返回一个指向 unique_ptr
的指针,这样就可以在不制作 copy 或放弃所有权的情况下访问 unique_ptr
的数据。
最后,移动构造函数 TrieNode(TrieNode &&other_trie_node)
用于将旧 TrieNode 的 unique pointer 传输到新的 TrieNode。确保在传输数据时没有复制唯一指针。
TrieNodeWithValue Class
TrieNodeWithValue
类继承自 TrieNode
,表示一个 terminal node,它的 key_char
是 key 的最后一个字符,该结点可保存任意类型 T
的 value ,is_end_
flag 始终为 true。
当使用给定的 key 迭代一个 trie 树并到达最终字符时,你将根据不同场景调用 TrieNodeWithValue
的不同构造函数(详见 Trie Class 部分)。现在,你需要知道的是,TrieNodeWithValue (char key_char,T value)
构造函数根据给定的 key 的字符和 value 构建一个 TrieNodeWithValue
。TrieNodeWithValue (TrieNode &&trieNode,T value)
构造函数从给定的 trieNode 中获取 unique_ptr
的所有权,并将它自己的 value_
设置为给定的值。
Trie Class
Trie
类定义支持 insert、search、remove 操作的实际 Trie 树。Trie 树的根节点是所有 key 的起始节点,它本身不应该存储任何键字符。
Insert
要在 Trie 树中插入一个kv,首先要使用给定的 key 遍历该 Trie 树,若不存在 TrieNode
,则插入。⚠️ 不允许插入一个重复的 key,应该返回 false。一旦到达 key 的最后一个字符,会有三种情况:
- 具有该字符的
TrieNode
不存在。则需要调用TrieNodeWithValue (char key_char,T value)
构造函数来创建具有指定 key_char 和 value 的 trie node。利用多态性 ——TrieNode
类的unique_ptr
也可存储TrieNodeWithValue
类的unique_ptr
。 - 具有该字符的
TrieNode
存在,但不是 terminal node(is_end_==false
)。这意味着unique_ptr
指向的是TrieNode
类对象而不是TrieNodeWithValue
类对象。需要调用TrieNodeWithValue (TrieNode &&trieNode,T value)
构造函数将旧TrieNode
对象转为新的TrieNodeWithValue
。 - 具有该字符的
TrieNode
存在,且已是 terminal node(is_end_==true
)这意味着unique_ptr
已经指向一个TrieNodeWithValue
类对象。应该立即返回 false,因为 key 不允许重复。
Remove
从 Trie 树中删除一个kv:
- 根据给定的 key 遍历该 Trie 树,若该 key 不存在,立即 return。
- 将 terminal node 的
is_end_
flag 变为 false。 - 若 terminal node 没有任何child结点,将它从父结点的
children_
map 中移除。 - 遍历 Trie 树并递归删除没有子结点的结点,当遇到有子结点的结点时就停止。
GetValue
对于给定的 key,返回对应的 value。若找不到 key 或提供的 type 与结点 value 的 type 不匹配,则将 success
设为 false。要检查这两种 type 是否相同,dynamic_cast raw TrieNode
pointer to TrieNodeWithValue<T>
pointer. 若转换结果为 nullptr
则 type T 与结点存储的 value 的类型不匹配。
Task #2 - Concurrent Trie
Trie 树需要保证 insert、search、remove 操作在多线程环境中工作。可使用 RwLatch
(BusTub 对 readers-writer lock 的实现)或 C++ STL 的std::shared_mutex
来实现。
这个项目只需要你通过获取根节点的 read/write lock 来实现简单的并发控制。
GetValue
函数应该从根结点获取一个 read lock(通过调用RwLatch
的RLock
方法)Insert
和Remove
操作应该从根结点获取一个 write lock(通过调用RwLatch
的WLock
方法)
若使用 RWLatch
,请确保在函数返回之前解锁所有获取的锁,以避免死锁。
Testing
可以使用我们的测试框架来测试此任务的各个组件。我们对单元测试用例使用 GTest。
test/primer/starter_trie_test.cpp
文件包含了对以上三个类的测试。
测试前要删掉测试文件中各函数前的 DISABLED_
前缀。
$ cd build
$ make starter_trie_test
$ ./test/starter_trie_test
提交
您将把您的实现提交给 Grescope:https://www.gradescope.com/courses/424375/
您只需在提交的 zip 文件中包含以下文件及其完整路径:
- src/include/primer/p0_trie.h
或者,在你的工作目录 (aka bustub
, bustub-private
, etc.) 运行👇 zip 命令,将创建一个名为 project0-submission.zip
的 zip 归档文件,你可以将它提交给 Gradescope。您还可以将此命令放在 bash 文件中,并运行 bash 文件,以使事情变得更容易。
$ zip project0-submission.zip \
src/include/primer/p0_trie.h
您可以使用 unzip 命令验证文件的内容。这个命令的输出应该如下所示:
$ unzip -l project0-submission.zip
Archive: project0-submission.zip
Length Date Time Name
-------- ---------- ----- ----
4465 2020-08-25 19:25 src/include/primer/p0_trie.h
-------- -------
4782 1 files
代码实现
TrieNode Class
class TrieNode {
public:
explicit TrieNode(char key_char) : key_char_(key_char) {}
TrieNode(TrieNode &&other_trie_node) noexcept
: key_char_(other_trie_node.key_char_),
is_end_(other_trie_node.is_end_),
children_(std::move(other_trie_node.children_)) {}
virtual ~TrieNode() = default;
bool HasChild(char key_char) const {
return children_.find(key_char) != children_.end();
}
bool HasChildren() const {
return !children_.empty();
}
bool IsEndNode() const {
return is_end_;
}
char GetKeyChar() const {
return key_char_;
}
std::unique_ptr<TrieNode> *InsertChildNode(char key_char, std::unique_ptr<TrieNode> &&child) {
if (HasChild(key_char) || child->GetKeyChar() != key_char) {
return nullptr;
}
children_[key_char] = std::move(child);
return &children_[key_char];
}
std::unique_ptr<TrieNode> *GetChildNode(char key_char) {
if (!HasChild(key_char)) {
return nullptr;
}
return &children_[key_char];
}
void RemoveChildNode(char key_char) {
if (!HasChild(key_char)) {
return;
}
children_.erase(key_char);
}
void SetEndNode(bool is_end) {
is_end_ = is_end;
}
protected:
char key_char_;
bool is_end_{false};
std::unordered_map<char, std::unique_ptr<TrieNode>> children_;
};
TrieNodeWithValue Class
template<typename T>
class TrieNodeWithValue : public TrieNode {
private:
T value_;
public:
TrieNodeWithValue(TrieNode &&trieNode, T value) : TrieNode(std::move(trieNode)) {
value_ = value;
is_end_ = true;
}
TrieNodeWithValue(char key_char, T value) : TrieNode(key_char) {
value_ = value;
is_end_ = true;
}
~TrieNodeWithValue() override = default;
T GetValue() const { return value_; }
};
Trie Class
class Trie {
private:
/* Root node of the trie */
std::unique_ptr<TrieNode> root_;
/* Read-write lock for the trie */
ReaderWriterLatch latch_;
public:
/**
* TODO(P0): Add implementation
*
* @brief Construct a new Trie object. Initialize the root node with '\0'
* character.
*/
Trie() {
root_ = std::make_unique<TrieNode>('\0');
}
/**
* TODO(P0): Add implementation
*
* @brief Insert key-value pair into the trie.
*
* If key is empty string, return false immediately.
*
* If key alreadys exists, return false. Duplicated keys are not allowed and
* you should never overwrite value of an existing key.
*
* When you reach the ending character of a key:
* 1. If TrieNode with this ending character does not exist, create new TrieNodeWithValue
* and add it to parent node's children_ map.
* 2. If the terminal node is a TrieNode, then convert it into TrieNodeWithValue by
* invoking the appropriate constructor.
* 3. If it is already a TrieNodeWithValue,
* then insertion fails and return false. Do not overwrite existing data with new data.
*
* You can quickly check whether a TrieNode pointer holds TrieNode or TrieNodeWithValue
* by checking the is_end_ flag. If is_end_ == false, then it points to TrieNode. If
* is_end_ == true, it points to TrieNodeWithValue.
*
* @param key Key used to traverse the trie and find correct node
* @param value Value to be inserted
* @return True if insertion succeeds, false if key already exists
*/
template<typename T>
bool Insert(const std::string &key, T value) {
if (key.empty()) {
return false;
}
latch_.WLock();
std::unique_ptr<TrieNode> *node = &root_;
for (size_t i = 0; i < key.length() - 1; i++) {
// 当前结点不存在 key[i] 对应的子结点
if (node->get()->GetChildNode(key[i]) == nullptr) {
std::unique_ptr<TrieNode> temp(new TrieNode(key[i]));
node->get()->InsertChildNode(key[i], std::move(temp));
}
node = node->get()->GetChildNode(key[i]);
}
std::unique_ptr<TrieNode> *terminal_node = node->get()->GetChildNode(key[key.length() - 1]);
// 不存在 terminal node
if (terminal_node == nullptr) {
std::unique_ptr<TrieNodeWithValue<T>> temp(new TrieNodeWithValue(key[key.length() - 1], value));
node->get()->InsertChildNode(key[key.length() - 1], std::move(temp));
latch_.WUnlock();
return true;
}
// terminal node is a TrieNode
if (!terminal_node->get()->IsEndNode()) {
TrieNodeWithValue<T> *temp = new TrieNodeWithValue(std::move(**terminal_node), value);
terminal_node->reset(temp);
latch_.WUnlock();
return true;
}
// already a TrieNodeWithValue
latch_.WUnlock();
return false;
}
/**
* TODO(P0): Add implementation
*
* @brief Remove key value pair from the trie.
* This function should also remove nodes that are no longer part of another
* key. If key is empty or not found, return false.
*
* You should:
* 1) Find the terminal node for the given key.
* 2) If this terminal node does not have any children, remove it from its
* parent's children_ map.
* 3) Recursively remove nodes that have no children and is not terminal node
* of another key.
*
* @param key Key used to traverse the trie and find correct node
* @return True if key exists and is removed, false otherwise
*/
bool Remove(const std::string &key) {
if (key.empty()) {
return false;
}
latch_.WLock();
std::vector<std::unique_ptr<TrieNode> *> pre_nodes;
std::unique_ptr<TrieNode> *node = &root_;
for (char i : key) {
pre_nodes.emplace_back(node);
node = node->get()->GetChildNode(i);
if (node == nullptr) {
latch_.WUnlock();
return false;
}
}
node->get()->SetEndNode(false); // 变为中间节点
for (int i = pre_nodes.size() - 1; i >= 0; --i) {
std::unique_ptr<TrieNode> *child = pre_nodes[i]->get()->GetChildNode(key[i]);
// child 是中间节点且没有子结点,就删除该 child
// !! 一定要判断child是不是中间节点,因为terminal node本身就没有子结点
if (!child->get()->HasChildren() && !child->get()->IsEndNode()) {
pre_nodes[i]->get()->RemoveChildNode(key[i]);
} else {
break;
}
}
latch_.WUnlock();
return true;
}
/**
* TODO(P0): Add implementation
*
* @brief Get the corresponding value of type T given its key.
* If key is empty, set success to false.
* If key does not exist in trie, set success to false.
* If given type T is not the same as the value type stored in TrieNodeWithValue
* (ie. GetValue<int> is called but terminal node holds std::string),
* set success to false.
*
* To check whether the two types are the same, dynamic_cast
* the terminal TrieNode to TrieNodeWithValue<T>. If the casted result
* is not nullptr, then type T is the correct type.
*
* @param key Key used to traverse the trie and find correct node
* @param success Whether GetValue is successful or not
* @return Value of type T if type matches
*/
template<typename T>
T GetValue(const std::string &key, bool *success) {
*success = false;
if (key.empty()) {
return {};
}
latch_.RLock();
std::unique_ptr<TrieNode> *node = &root_;
for (char i : key) {
node = node->get()->GetChildNode(i);
if (node == nullptr) {
latch_.RUnlock();
return {};
}
}
T res;
auto terminal_node = dynamic_cast<TrieNodeWithValue<T> *>(node->get());
if (terminal_node == nullptr) {
res = {};
} else {
*success = true;
res = terminal_node->GetValue();
}
latch_.RUnlock();
return res;
}
};
出现的问题
1️⃣ 内存泄露 for (size_t i = N; i >= 0; i--)
因为 size_t 是无符号的,因此当i = 0 时 i-- 永远不会小于零。如果限制写 i>0 还是不会有内存泄露的。
解决:还是使用 for (int i = N; i >= 0; i--)
2️⃣ Trie 的 Remove(const std::string &key) 方法中,要判断child结点为中间节点且没有children才能 RemoveChildNode。因为可能存在这种情况:
bool success = trie.Insert<int>("a", 5);
EXPECT_EQ(success, true);
success = trie.Insert<int>("aa", 6);
EXPECT_EQ(success, true);
success = trie.Insert<int>("aaa", 7);
EXPECT_EQ(success, true);
success = trie.Remove("aaa");
EXPECT_EQ(success, true);
root
|
aa->5
|
aa->6
|
aaa->7
此时我们将 terminal node 变为中间结点(is_end_=false
)然后回溯进行删除子结点操作:
- aa->6 存在一个child结点 aaa->7,该结点是中间结点且无 children,因此删除该child结点
- aa->5 存在一个child结点 aa->6,该结点虽然无 children,但是 terminal node,因此不用删除
(⚠️若此时不判断是不是中间结点,只判断该结点有无children,就会误删该结点)
测试提交结果
测试的时候要删掉test/primer/starter_trie_test.cpp
测试文件中各函数前的 DISABLED_
前缀。
$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Debug ..
$ make -j4
$ make starter_trie_test
$ ./test/starter_trie_test

# 运行代码格式化和规范检查,必须改正所有提示,否则后续提交评分为 0
$ make format
$ make check-lint
$ make check-clang-tidy-p0
# 到达工作目录bustub
$ cd ../
$ zip project0-submission.zip \
src/include/primer/p0_trie.h
# 验证压缩文件内容
$ unzip -l project0-submission.zip
Archive: project0-submission.zip
Length Date Time Name
--------- ---------- ----- ----
13097 11-16-2022 16:14 src/include/primer/p0_trie.h
--------- -------
13097 1 file

本文作者:Joey-Wang
本文链接:https://www.cnblogs.com/joey-wang/p/16916598.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步