Project #0 - C++ Primer
- 一、题目链接
- 二、准备工作
- 1.项目构建
- 2.代码测试
- 3.代码格式化
- 4.压缩与提交
- 三、部分实现
一、题目链接
二、准备工作
以下操作在题目文档中均有提及,这里进行简要整理。
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-lint
和 make 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一样给出失败的测试用例。
不过我们可以通过以下的函数从在线测试平台上获取测试文件的源代码,从而查看测试用例。我们只需要将对应函数放在 p0_trie.h
文件中,然后在任意一个函数中调用 ShowTestCase()
即可,这里的测试文件文件名 testfile
可以参考在线测试平台的输出以及本地目录结构。
#define SEP "====================================================================================================="
void ShowTestCase() {
static bool tag = true; // 通过静态变量保证只输出一次
if (tag) {
std::string testfile = "/autograder/bustub/test/primer/grading_starter_trie_test.cpp"; // 文件名
std::ifstream in_file; // 读文件流
char line_buffer[256] = {0}; // 行缓冲区
in_file.open(testfile, std::ios::in); // 以读模式打开文件
if (in_file) { // 文件打开成功
std::cout << "\n\n\n\n\n" << SEP << "\n" << testfile << "\n" << SEP << std::endl;
while (in_file.getline(line_buffer, sizeof(line_buffer))) {
std::cout << line_buffer << std::endl;
}
std::cout << SEP << "\n\n\n\n\n" << std::endl;
in_file.close();
} else { // 文件打开失败
std::cout << "\n\n\n\n\n" << SEP << "\n" << "can not open \"" << testfile << "\".\n" << SEP << "\n\n\n\n\n"
<< std::endl;
}
tag = false;
}
}
三、部分实现
Insert
template<typename T>
bool Insert(const std::string &key, T value) {
std::unique_lock <std::shared_mutex> lock(shared_mutex_); // 通过unique_lock使用互斥锁
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); // 待插入位置的节点
if ((*next_node)->IsEndNode()) { // key对应的位置已经存在一个值节点了
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
bool Remove(const std::string &key) {
std::vector < std::unique_ptr < TrieNode > * > records; // 记录访问过的节点,用于后续删除
std::unique_lock <std::shared_mutex> lock(shared_mutex_); // 通过unique_lock使用互斥锁
/* 按照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>
T GetValue(const std::string &key, bool *success) {
std::shared_lock <std::shared_mutex> lock(shared_mutex_); // 通过shared_lock使用共享锁
*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();
}