CMU 15-445 Project #0 - C++ Primer

在这里插入图片描述

一、题目链接


二、准备工作

以下操作在题目文档中均有提及,这里进行简要整理。

1.项目构建

首先需要从远程仓库克隆项目文件,由于该仓库会随每年的课程一起更新,所以需要根据课程时间指定相应分支。

git clone --branch v20221128-2022fall https://github.com/cmu-db/bustub.git

下载下来后需要在项目根目录下执行 build_support/packages.sh 安装BusTub需要的包,具体执行哪个脚本因操作系统而异。

随后在项目根目录执行以下命令,通过cmake构建项目,这里的 -DCMAKE_BUILD_TYPE=Debug 表示在调试模式下构建项目,即在没有优化的情况下,使用带有调试符号的方式构建库或可执行文件。

mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make

2.代码测试

本项目通过GoogleTest完成代码测试及评分,题目文件为 src/include/primer/p0_trie.h,测试文件为 test/primer/starter_trie_test.cpp,测试文件中的每一个 TEST 对应一个测试单元。

我们在刚才创建的build目录下,通过以下命令执行测试,这里需要注意的是,需要去掉测试文件的代码中的 DISABLED_ 前缀以激活对应测试单元。

make starter_trie_test
./test/starter_trie_test

执行运行命令后控制台会打印测试结果,[ OK ] 即对应单元测试通过,如果有 [ DISABLED ] 则表示对应单元中的 DISABLED_ 前缀没有删除。

atreus@MacBook-Pro % ./test/starter_trie_test
Running main() from gmock_main.cc
[==========] Running 5 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from StarterTest
[ RUN      ] StarterTest.TrieNodeInsertTest
[       OK ] StarterTest.TrieNodeInsertTest (0 ms)
[ RUN      ] StarterTest.TrieNodeRemoveTest
[       OK ] StarterTest.TrieNodeRemoveTest (0 ms)
[ RUN      ] StarterTest.TrieInsertTest
[       OK ] StarterTest.TrieInsertTest (0 ms)
[----------] 3 tests from StarterTest (0 ms total)

[----------] 2 tests from StarterTrieTest
[ RUN      ] StarterTrieTest.RemoveTest
[       OK ] StarterTrieTest.RemoveTest (0 ms)
[ RUN      ] StarterTrieTest.ConcurrentTest1
[       OK ] StarterTrieTest.ConcurrentTest1 (184 ms)
[----------] 2 tests from StarterTrieTest (184 ms total)

[----------] Global test environment tear-down
[==========] 5 tests from 2 test suites ran. (184 ms total)
[  PASSED  ] 5 tests.
atreus@MacBook-Pro % 

我们也可以自己指定测试用例:

TEST(StarterTest, MyTest) {
  Trie trie;
  bool success;
  success = trie.Insert<int>("a", 1);
  EXPECT_EQ(success, true);
  EXPECT_EQ(trie.GetValue<int>("a", &success), 1);

  success = trie.Insert<int>("abc", 3);
  EXPECT_EQ(success, true);
  EXPECT_EQ(trie.GetValue<int>("abc", &success), 3);

  success = trie.Insert<int>("ab", 2);
  EXPECT_EQ(success, true);
  EXPECT_EQ(trie.GetValue<int>("ab", &success), 2);

  success = trie.Remove("ab");
  EXPECT_EQ(success, true);
  trie.GetValue<int>("ab", &success);
  EXPECT_EQ(success, false);

  success = trie.Insert<int>("ab", 5);
  EXPECT_EQ(success, true);
  EXPECT_EQ(trie.GetValue<int>("ab", &success), 5);
}

3.代码格式化

代码最终要提交到在线网站,如果代码风格不遵循Google C++ Style Guide会直接被判零分,因此我们需要对代码进行格式化。

在build目录下执行 make format 通过python脚本自动格式化代码,然后执行 make check-lintmake check-clang-tidy-p0 检查格式化结果,控制台将打印代码格式问题,依次进行修改即可。

如果完成格式话要求的话输出大致如下:

atreus@MacBook-Pro % make format             
Built target format
atreus@MacBook-Pro % make check-lint         
Built target check-lint
atreus@MacBook-Pro % make check-clang-tidy-p0
Enabled checks:
    bugprone-argument-comment
    bugprone-assert-side-effect
    bugprone-bad-signal-to-kill-thread
    ......
    readability-uniqueptr-delete-release
    readability-uppercase-literal-suffix
    readability-use-anyofallof

Checking: /Users/atreus/CLionProjects/bustub/src/primer/p0_trie.cpp

Built target check-clang-tidy-p0
atreus@MacBook-Pro % 

4.压缩与提交

我们通过 zip 命令压缩 p0_trie.h 文件。

这里需要注意的是,压缩文件时文件名中需要包含完整的 src/include/primer/p0_trie.h 路径,因此我们在项目根目录下进行压缩:

atreus@MacBook-Pro % zip project0-submission.zip ./src/include/primer/p0_trie.h  # 压缩
updating: src/include/primer/p0_trie.h (deflated 72%)
atreus@MacBook-Pro % unzip -l project0-submission.zip  # 检查压缩文件内容
Archive:  project0-submission.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    13702  05-31-2023 00:00   src/include/primer/p0_trie.h
---------                     -------
    13702                     1 file
atreus@MacBook-Pro % 

代码需要提交到以下网址进行评测,提交前需要以CMU进行注册:

https://www.gradescope.com/courses/424375/

如果评测成功结果大致如下:

在这里插入图片描述

其实这个关于线上测试有个小技巧,因为线上评测的用例比本地提供的要复杂一些,但是它只会提供给你最终结果,而不是像LeetCode一样给出失败的测试用例。

不过我们可以通过 system("cat /autograder/bustub/test/primer/grading_starter_trie_test.cpp"); 从在线测试平台上获取测试文件的源代码,从而查看测试用例。我们只需要在 p0_trie.h 文件的任意一个函数中执行上述语句即可,这里的文件名可以参考在线测试平台的输出以及本地目录结构。


三、部分实现

Insert

template<typename T>
auto Insert(const std::string &key, T value) -> bool {
    /* 通过unique_lock使用互斥锁 */
    std::unique_lock<std::shared_mutex> lock(shared_mutex_);

    if (key.empty()) {
        return false;
    }

    auto cur_node = &root_;
    for (int i = 0; i < static_cast<int>(key.size()); i++) {
        char cur_c = key.at(i);  // 本次要插入的字符

        if (i == static_cast<int>(key.size()) - 1) {
            /* 如果待插入位置已经存在一个节点了,需要判断它是不是值节点,否则直接插入。 */
            if ((*cur_node)->HasChild(cur_c)) {
                auto next_node = (*cur_node)->GetChildNode(cur_c);  // 待插入位置的节点

                /* key对应的位置已经存在一个值节点了 */
                if ((*next_node)->IsEndNode()) {
                    return false;
                }

                /* 用一个TrieNodeWithValue节点替换这个TrieNode节点 */
                auto new_node = std::make_unique<TrieNodeWithValue < T >>
                (std::move(*(*next_node)), value);
                (*cur_node)->RemoveChildNode(cur_c);
                (*cur_node)->InsertChildNode(cur_c, std::move(new_node));
            } else {
                (*cur_node)->InsertChildNode(cur_c, std::make_unique<TrieNodeWithValue < T >>
                (cur_c, value));
            }
        } else {
            /* 如果当前字符对应的节点已经存在直接向下遍历,否则需要先插入再访问。 */
            if ((*cur_node)->HasChild(cur_c)) {
                cur_node = (*cur_node)->GetChildNode(cur_c);
            } else {
                cur_node = (*cur_node)->InsertChildNode(cur_c, std::make_unique<TrieNode>(cur_c));
            }
        }
    }

    return true;
}

Remove

auto Remove(const std::string &key) -> bool {
    std::vector<std::unique_ptr<TrieNode> *> records;  // 用于记录访问过的节点,便于后续删除

    /* 通过unique_lock使用互斥锁 */
    std::unique_lock<std::shared_mutex> lock(shared_mutex_);

    /* 按照key查找节点 */
    records.emplace_back(&root_);
    for (int i = 0; i < static_cast<int>(key.size()); i++) {
        std::unique_ptr<TrieNode> *next_node = (*records.at(i))->GetChildNode(key.at(i));

        /* 满足条件的子节点不存在,查找失败。 */
        if (next_node == nullptr) {
            return false;
        }

        /* 将每一个访问过的节点入队 */
        records.emplace_back(next_node);
    }

    /* key对应的节点为一个非值节点,查找失败。 */
    if (!(*records.at(static_cast<int>(records.size()) - 1))->IsEndNode()) {
        return false;
    }

    /* 从下到上依次删除节点 */
    for (int i = static_cast<int>(records.size()) - 1; i > 0; i--) {
        /* 如果待删除的节点有孩子,只需要将is_end_置为false即可,否则需要从父节点中删除当前节点。
         * 这里一个更好的做法是将这个TrieNodeWithValue节点转换为TrieNode节点,这样能节省一定内存。 */
        if ((*records.at(i))->HasChildren()) {
            (*records.at(i))->SetEndNode(false);
        } else {
            (*records.at(i - 1))->RemoveChildNode(key.at(i - 1));
        }

        /* 下一个要处理的节点为值节点,停止删除。 */
        if ((*(*records.at(i - 1))).IsEndNode()) {
            break;
        }
    }

    return true;
}

GetValue

template<typename T>
auto GetValue(const std::string &key, bool *success) -> T {
    /* 通过shared_lock使用共享锁 */
    std::shared_lock<std::shared_mutex> lock(shared_mutex_);

    *success = true;

    if (key.empty()) {
        *success = false;
        return {};
    }

    /* 按照key查找节点 */
    std::unique_ptr<TrieNode> *cur_node = &root_;
    for (char c : key) {
        cur_node = (*cur_node)->GetChildNode(c);
        if (cur_node == nullptr) {
            *success = false;
            return {};
        }
    }

    /* 查找失败 */
    if (!(*cur_node)->IsEndNode()) {
        *success = false;
        return {};
    }

    /* 检查范型T和实际存储的数据类型是否一致 */
    auto result = dynamic_cast<TrieNodeWithValue <T> *>(&(*(*cur_node)));
    if (result == nullptr) {
        *success = false;
        return {};
    }

    return result->GetValue();
}

参考:

https://zhuanlan.zhihu.com/p/619220493
https://cloud.tencent.com/developer/article/2286131
https://www.cnblogs.com/suqinglee/p/16684055.html

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值