CMU15/445 2023 Spring-project0 基于字典树的增删改查

1 前言

配置按照文档照做即可,有一点需要注意:

我的环境是ubuntu22.4,需要提前安装 cmake clang-14 gdb g++

sudo apt install cmake clang-14 gdb g++

配置根目录下的CMakeLists

set(CMAKE_C_COMPILER "/usr/bin/clang-14")
set(CMAKE_CXX_COMPILER "/usr/bin/clang++-14")

2 Task #1 - Copy-on-Write 尝试

原项目的图不够清晰明白,笔者这张图比较直观。但是也没有体现完全复用的思路。

首先先用到三个类:TrieNode、TrieNodeWithValue和Trie。

其中TrieNode是父类:每个节点包含一个子节点的映射 children_,其中键是下一个字符,值是下一个 TrieNode 节点。还包含一个布尔值 is_value_node_,表示该节点是否是一个终止节点。

TrieNodeWithValue是子类:包含一个值的指针 value_,表示该节点关联的值。

Trie是前缀树类。

在本任务中,您需要修改trie .h和trie .cpp来实现写时复制(copy-on-write) tree。在copy-on-write tree中,操作不会直接修改原始tree中的节点。相反,将为修改后的数据创建新节点,并为新修改的trie返回一个新根。写时复制(Copy-on-write)使我们能够在每次操作之后随时以最小的开销访问该树。考虑在上面的例子中插入("ad", 2)。我们通过重用原始树中的两个子节点来创建一个新的Node2,并创建一个新的值节点2。(见下图)

如果我们接着插入("b", 3),我们将创建一个新的根,一个新的节点,并重用以前的节点。通过这种方式,我们可以在每次插入操作之前和之后获得树的内容。只要我们有根对象(Trie类),我们就可以访问当时Trie中的数据。(见下图)

再举一个例子:如果我们插入(“a”,“abc”)并删除(“ab”,1),我们可以得到下面的trie。注意,父节点可以有值,并且在删除后需要清除所有不必要的节点。空树由nullptr表示。

您的尝试必须支持三种操作:

  • Get(key):获取键对应的值。

思路:沿着父节点往下移动,找到带数据的节点时,使用dynamic_cast转换成子点

这样似乎更好理解:定义一个指向头节点的TireNode指针,这个指针不为空时,从头到尾遍历key,然后这个指针一直往下一个节点移动。当遍历结束时,要么指针为空,也就是没有找到;要么就找到这个叶子节点,然后将这个指针利用dynamic_pointer_cast转换成子类指针,获取它的value返回即可。

  •  Put(key, value):设置键对应的值。如果键已经存在,则覆盖现有值。注意值的类型可能是不可复制的(即std::unique_ptr<int>)。这个方法返回一个新的尝试。

思路:

case1: 找不到该key,且此时遍历到键的最后一个字符,直接在尾部创建一个带值的TrieNodeWithValue;
case2: 找不到该key,且此时在中间,创建一个TireNode;
case3: 找到该key,且是键的最后一个字符,直接替换成对应的value;
case4: 找到该key,且是中间节点,继续向下移动。

这样似乎更好理解:如果根节点为空,创建一个新根节点;否则克隆原来的根节点;然后从根结点开始遍历,key中的指针也往后移动,遇到不存在的c,就创建节点,,存在就一直遍历,直到key遍历到最后,如果还是不存在就创建新节点;否则就是替换value。

  • Delete(key):删除键的值。这个方法返回一个新的尝试。

遍历键的每个字符,在 Trie 中寻找对应的节点。如果找不到对应节点,则直接返回当前的 Trie 对象,因为无需进行任何操作。

如果找到了对应节点,且当前字符是键的最后一个字符,则需要判断该节点是否为叶子节点。如果是叶子节点;则将该节点删除;否则,将该节点转换为普通的 TrieNode 节点。

如果当前字符不是键的最后一个字符,则继续向下移动,处理下一个字符。

最后,返回经过修改后的 Trie 对象。

3 Task #2 - Concurrent Key-Value Store

在拥有可用于单线程环境的写时复制trie之后,为多线程环境实现并发键值存储。**在本任务中,您需要修改trie_store .h和trie_store .cpp。**这个键值存储还支持3种操作:

- `Get(key)` returns the value.
- `Put(key, value)`. No return value.
- `Delete(key)`. No return value.

Get思路:
(1)对根节点进行加锁;
(2)获取根节点的副本;
(3)释放根节点的锁,以便其他线程访问;
(4)查找数据。

Put思路:
(1)获得写锁,写入数据;
(2)对根节点进行加锁,更新根节点;
(3)释放根节点的锁,释放写锁。

Delete思路:
(1)获得写锁,删除数据;
(2)对根节点进行加锁,更新根节点;
(3)释放根节点的锁,释放写锁。

4 Task #3 - Debugging

在本任务中,您将学习调试的基本技术。你可以选择任何你喜欢的调试方式,包括但不限于:使用cout和printf,使用CLion / VSCode调试器,使用gdb等。

请参考trie_debug_test .cpp获取说明。在生成trie结构并回答几个问题之后,您需要设置一个断点。您需要在trie_answer .h中填写答案。

我使用的是VS Code,学习一下linux下VS code CMake的调试

前面已经安装了clang-14,继续安装C/C++的插件C/C++ Extension,接着ctrl+shift+p,选择CMake: Select a Kit

再选择Clang

此时VS Code下栏如下图中1所示,然后在2处选择要调试的文件【trie_debug_test.cpp 】,记得打断点,然后点击4进行调试。

调试结果如果所示:

但是我这里提交一直有问题,于是乎,网上各种查找,找到这篇文章:CMU 15-445 Project 0 (Spring 2023) | 可持久化字典树 (Copy-On-Write Trie) - 知乎 (zhihu.com)

可能是是因为本地和 gradescope 的随机数不一样导致。修改trie_debug_test.cpp前面随机生成Tire部分

  auto trie = Trie();
  trie = trie.Put<uint32_t>("65", 25);
  trie = trie.Put<uint32_t>("61", 65);
  trie = trie.Put<uint32_t>("82", 84);
  trie = trie.Put<uint32_t>("2", 42);
  trie = trie.Put<uint32_t>("16", 67);
  trie = trie.Put<uint32_t>("94", 53);
  trie = trie.Put<uint32_t>("20", 35);
  trie = trie.Put<uint32_t>("3", 57);
  trie = trie.Put<uint32_t>("93", 30);
  trie = trie.Put<uint32_t>("75", 29);

But,我这个调试处理结果又对不上。。。。。。。

用上面的分析这颗Tire,注意要仔细分析,然后回答问题,这里我又踩坑了。

5 Task #4 - SQL String Functions

现在是时候深入了解BusTub本身了!您将需要实现上层和下层SQL函数。这可以通过两个步骤完成:(1)在string_expression.h中实现函数逻辑。(2)在BusTub中注册函数,以便SQL框架可以在用户执行SQL时调用你的函数,在plan_func_call.cpp中。

要测试你的实现,你可以使用bustub-shell:

cd build
make -j`nproc` shell
./bin/bustub-shell
bustub> select upper('AbCd'), lower('AbCd');
ABCD abcd

(1)这里要实现string_expression.h中的Compute,即实现大小写的转换。开始我写的很啰嗦,后面想简化代码,查查有没有STL/库函数(toupper、tolower)之类的,还真有一个std::transform

template <class InputIterator, class OutputIterator, class UnaryOperation>
  OutputIterator transform (InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperation op);

对于一元操作,将op应用于[first1, last1)范围内的每个元素,并将每个操作返回的值存储在以result开头的范围内。给定的op将被连续调用last1-first1次。op可以是函数指针或函数对象或lambda表达式。

于是乎,优化也。

(2)在BusTub中注册函数,以便SQL框架可以在用户执行SQL时调用你的函数,在plan_func_call.cpp中。

实现GetFuncCallFromFactory函数,这里要结合这里的命令来分析

cd build
make -j`nproc` shell
./bin/bustub-shell
bustub> select upper('AbCd'), lower('AbCd');
ABCD abcd

判断输入的参数名及个数进行调用。

6 测试通过

7 问题

(1)每次提交前检查一下代码,格式不对,一个也过不了。

make format
make check-clang-tidy-p0

(2)总的来说,2023 sping project0要比之前2021 fail矩阵难好几个维度,有劝退的意思,坚持慢慢看源码/注释+画图,也对智能指针有了更深的理解。

参考:

[1] Project #0 - C++ Primer | CMU 15-445/645 :: Intro to Database Systems (Spring 2023)

[2] CMU_15_445_project_0_C++_Primer - autumn814 - 博客园 (cnblogs.com)

[3] 【用法总结】C++中常用的大小写转换(4种常用方法)_c++大写字母转小写字母-CSDN博客

[4] C++/C++11中std::transform的使用_c++transform-CSDN博客

[5] CMU 15445 vscode/clion clang12 cmake环境配置 - 知乎

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值