首页 > 编程语言 >【CMU15-445 FALL 2022】Project #0 - C++ Primer

【CMU15-445 FALL 2022】Project #0 - C++ Primer

时间:2023-04-22 19:42:47浏览次数:74  
标签:字符 char 结尾 445 Project C++ key unique 节点


关于

参考 & 鸣谢


前言

  • 按照课程要求,本文并不会给出实现代码,可以当做是我遇到问题的总结,一些理解 & 解释,希望能帮助到需要的读者。
  • 具体的信息可以去课程官网的对应实验说明处寻找。
  • 实验使用C++实现,设定的标准是C++17,对C++语法不了解的小伙伴需要自行学习下,Project 0中需要的一些用法在本文中会有所标注,需要特别强调的在【补充】处说明。
  • 还是希望先自己尝试去做,如果没有思路,再来参考我的实现思路。

环境配置

  • clone指定版本,因为官方github该仓库每年都会进行更新。(从群内的聊天记录中翻到的。)
  • 我们需要的是 Faill 2022最后一版
git clone --branch v20221128-2022fall https://github.com/cmu-db/bustub.git
  • 这里我使用CLion + WSL作为本项目的开发环境,环境配置详见上方引出来的环境配置文章。
  • 这两个一开,跑项目时还是挺占内存的。

测试 & 调试

测试

某一个模块的代码我们写完后需要进行测试,项目中有用GTest写好的测试程序,将第二个参数的**DISABLE_**前缀去掉即可运行。
通过本地测试用例后,提交到测试网站上进行最终的评分。

  • 这里还是推荐使用CLion,它可以只运行某一个测试用例。其他的貌似只能通过编译运行整个文件。

【CMU15-445 FALL 2022】Project #0 - C++ Primer_#include


调试

  • 我依然是使用CLion内进行打断点调试。
  • 在项目的顶级CMakeLists.txt中添加,如下行代码,以便于可以在调试时显示更多信息。
  • 如下图所示
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_GLIBCXX_DEBUG")

【CMU15-445 FALL 2022】Project #0 - C++ Primer_#include_02

  • 调试按钮如下图所示

【CMU15-445 FALL 2022】Project #0 - C++ Primer_字符串_03

  • 从左到右分别是
  • 执行一行
  • 进入这行
  • 用的少,不是很了解
  • 从这个进入中出来
  • 这是我大致的理解,其实自己打个断点用用就会了,具体请网上自行搜索。

提交

注意在提交前需要先检测代码风格,否则提交上去是没分的。

  • 如果你跟我一样是直接使用CLion编译运行的,首先进入到项目文件夹/bustub文件夹下,依次执行如下命令,编译项目。
mkdir build
cd build
cmake ..
make
  • 然后执行格式检查
  • 应该要安装clang-fromat,clang-tidy,自行网上搜索安装。
  • 然后依次执行以下命令
make format
make check-lint
make check-clang-tidy-p0
  • 如果代码风格不符合要求,如下图所示,会给出错误说明。

【CMU15-445 FALL 2022】Project #0 - C++ Primer_c++_04


这里说明我遇到的一些格式问题

某一块语句要写在{}内

// 将
if(a == 1)return ;
// 替换为
if(a == 1){
    return 0;
}

if else语句中 if返回了,不能使用else

// 将
if(a == 1){
	return 0;
}else{
	std::cout << "test" <<std::endl;
}
// 替换为
if(a == 1){
	return 0;
}
std::cout << "test" <<std::endl;

判断语句尽可能的要写简洁

// 使用
if(!cur->get()->IsEndNode())
// 替换
if(cur->get()->IsEndNode() == false)

函数返回值使用auto类型,注意: 如果函数的返回类型依赖于函数体中的操作,后置声明语法是必要的。

auto InsertChildNode(char key_char, std::unique_ptr<TrieNode> &&child) -> std::unique_ptr<TrieNode> * {
    // 略
  }
  • 根据提示修改完成后,代码风格全部符合要求后,再次进行检测,结果如下图所示。

【CMU15-445 FALL 2022】Project #0 - C++ Primer_字典树_05


然后提交到测试网站上

测试网站 —— https://www.gradescope.com/courses/425272

  • 需要先注册账号,注册账号时学校选择CMU(全称),详见其他博主的文章。
  • 然后将代码打包
zip project0-submission.zip \     src/include/primer/p0_trie.h
  • 可以通过以下命令验证文件内容,结果如下图所示
unzip -l project0-submission.zip

【CMU15-445 FALL 2022】Project #0 - C++ Primer_c++_06

  • 最终在测试网站上选择对应的lab,将压缩包上传即可,然后等待结果。
  • 如下图所示,通过全部测试,100分啦。

【CMU15-445 FALL 2022】Project #0 - C++ Primer_#include_07


实验

OK,现在我们进入到具体的实验环节,我会先说明实验要干什么,然后依次给出一些说明和解释。

实验要求

根据给出的代码,实现一个可满足并发要求的字典树,相关类的的代码已经在/bustub/src/include/primer/p0_trie.h中给出,需要我们给出具体函数的定义,可以在其中添加一些需要的辅助变量/函数。

什么是字典树?

字典树又称前缀树,是一种有序树,用于保存关联数组,其中的键通常是字符串.——Wiki百科-Trie

  • 通俗的来说,就是将一串字符串依次拆分成字符存储到一棵的节点上,依次相连,前一个字符是后一个字符的父亲。从这个树中,查找是否有对应的字符串。
  • 字典是干嘛的,就是方便我们寻找对应的字的解释的。
  • 具体到本项目中就是存储对应的string及其value
  • 解释一下查找指定字符串过程
  • 在查找的时候从根节点的孩子开始查找;
  • (因为本项目的实现,根节点不存储数据,所以我们下面都以这个情况进行解释。)
  • 从孩子中寻找对应的下一个字符,沿着树向下靠近,直到找到对应的结尾字符,如果不存在该结尾字符或者该存在该结尾字符,但是它并没有被标记为【结尾节点】,说明这个字符串也不存在于我们这棵字典树上。
  • 反之,沿着节点找到了该字符串的所有字符,并且结尾字符被标记为【结尾节点】,说明找大了这个字符串,存在于我们这棵字典树上。

如下图所示

  • 这棵字典树中存储了,“abc”,“abcd”,“ace”,“ef”,这三个字符串。
  • 其中 ‘c’,‘d’,‘e’,‘f’,被标记为是结尾字符。
  • 注意重复的情况,如果字符串的开头可以覆盖,则可以共用前面的节点,如"abcd",“abc”,“ace”;
  • 反之,需要另起开头,如"ef"。

【CMU15-445 FALL 2022】Project #0 - C++ Primer_字典树_08

到此,我们对字典树有了一个大致的了解,这就足以实现我们本次的实验了。


实验内容

Task #1 - Templated Trie

任务一让我们实现的有关字典树的三个类

  • TrieNode
  • 字典树节点类
  • TrieNodeWithValue
  • 继承自TrieNode,特指结尾字符
  • Trie
  • 字典树类

TrieNode

成员变量

  • key_char: 存储当前节点所存的字符
  • is_end_: 当前节点对应的字符是否是存储的某一字符串的结尾字符
  • chaildren_: 存储当前节点孩子对应的char,以及指向它的指针,使用哈希表存储

成员函数

  • TrieNode(char);
  • 构造函数,使用一个char构造当前节点
  • TrieNode(TrieNode &&);
  • 移动构造函数,使用一个TrieNode来构造本TrieNode
  • 通过移动语义构造对象,避免不必要复制操作,以提高代码效率。
  • bool HashChild(char key_char);
  • 检查当前节点是否存在某个节点对应的字符为key_char的节点
  • bool HashChild();
  • 上一个函数的重载函数,检查当前节点有没有孩子。
  • bool IsEndNode()
  • 判断当前节点是否是结尾字符节点
  • char GetKeyChar();
  • 返回当前节点的对应的字符
  • std::unique_ptr * InsertChildNode(char key_char, std::unique_ptr &&child);
  • 插入到当前节点后面,即放入到当前节点的children_中
  • 不允许重复,如果要放入的key_char已经存在了,return nullptr
  • 反之,返回指向对应节点的unique_ptr 指针
  • std::unique_ptr * GetChild(char key_char);
  • 根据给定的key_char在当前节点的孩子中寻找这个节点,找到返回指向其的unique_ptr *指针,反之返回nullptr
  • void RemoveChildNode(char key_char);
  • 删除key_char对应的节点,在当前节点的孩子中寻找
  • void SetEndNode(bool is_end) ;
  • 设置当前节点的结尾字符标记为is_end

TrieNodeWithValue

  • 继承自TrieNode

成员变量

  • value_: 存储当前节点对应的value_,结尾节点才存储。即该字符串对应的value

成员函数

  • TrieNodeWithValue(TrieNode &&trieNode, T value);
  • 构造函数
  • 使用一个TrieNode节点和一个value值构造
  • 实际上其中调用的就是TrieNode的移动构造函数
  • TrieNode(std::move(trieNode));
  • TrieNodeWithValue(char key_char, T value);
  • 构造函数
  • 使用一个key_char 和一个value构造。
  • T GetValue();
  • 返回当前结尾节点对应的value

Trie

  • 字典树类。

成员变量

  • root_
  • 一颗字典树的根节点。不存储任何数据。
  • 注意: 根节点需要创建,C++11开始可以直接在成员变量声明处,直接初始化。
std::unique_ptr<TrieNode> root_ = std::make_unique<TrieNode>(' ');
  • latch_
  • 当前字典树的读写锁,对shared_mutex进行了一层封装。用于并发访问控制。

成员函数

**bool Insert(const std::string &key, T value); **

  • 在当前字典树上插入一个单词(key)
  • 实现思路
  • 遍历key的每一个字符,同时从root_孩子开始遍历,找到对应字符就继续往下走,反之为当前字符创建节点,存储到当前节点的孩子中, 更新遍历指针。
  • 最终到达末尾,判断是否已经被标记为结尾字符节点,因为不能重复,如果已被标记,返回false;
  • 未被标记,将当前TrieNode节点转换为TrieNodeWithValue节点。
  • 注意
  • 判断key是否为空
  • 注意创建root_
  • 使用unique_ptr的问题,这里给出提示使用auto cur = &root_; cur为unique_ptr 指针,调用get(),获取TrieNode裸指针,详见下方补充。
  • 插入过程如下面动画所示

【CMU15-445 FALL 2022】Project #0 - C++ Primer_c++_09

  • 最终结果如下图所示

【CMU15-445 FALL 2022】Project #0 - C++ Primer_#include_10


bool Remove(const std::string &key);

  • 在字典树中删除对应的字符串
  • 实现思路
  • 遍历每个字符,如果没找全,说明不存在,return false
  • 找全了,但是结尾字符,没有被标记为结尾字符,return false
  • 找到指定字符串了,结尾字符所在节点也被标记了,开始进行删除。
  • 把结尾字符节点标记为false
  • 在遍历每个字符之前,这里我使用一个vector<unque_ptr*>nodes保存走过的路径。
  • 即,保存,该key的每个字符的父节点。用于后序删除操作。
  • 开始删除
  • 反向遍历nodes,同时判断当前要删除的节点,如果被标记为了结尾节点,或者还有孩子。就不能删除了,删除过程终止。
  • 反之,不断从父节点的children_中,将该孩子的映射删除,一层一层向上朝root_前进。

做动画太累了,大家结合上面插入后的图理解一下吧。

  • 用两个例子简单说明下

删除abcd

【CMU15-445 FALL 2022】Project #0 - C++ Primer_字符串_11

  • 遍历过程省略
  • 将d节点 is_end_置为false。
  • 判断不是结尾字符啦,同时也没有孩子,将其从c几点的children_中删除,所对应内存会被自动释放。
  • 再判断c节点,虽然没有孩子,但是被标记为了结尾节点,过程终止。删除完毕。

删除ef

【CMU15-445 FALL 2022】Project #0 - C++ Primer_字符串_12

  • 过程同上。
  • 由于e没有孩子了,也不是结尾字符,所以它也会被从root_的children_中删除。

*T GetValue(const std::string &key, bool success);

  • 查找指定字符串是否在
  • 遍历字符沿着字典树寻找。
  • 找全了,并且结尾字符被置为结尾字符节点,说明找到,反之没找到。
  • 相比与前面两个,这个就简单多了,略…

Task #2 - Concurrent Trie

任务二在上面的基础上,对字典树类Trie加锁,实现并发访问。

  • 根据函数目的,调用读锁还是写锁。
  • 注意每个return 前记得开锁。

补充

unique_ptr

避免所有权转义

  • 使用unique_ptr指针,例如unique_ptr* p
  • 或者,使用get方法获取其裸指针
unique<TrieNode> p;
auto t = p.get();

容易混淆的一点,unique_ptr* 问题

// a 是 unique_ptr<int>
auto a  = make_unique<int>(32);//c++14
// b是 unque_Ptr<int>*
auto b = & a;
// 注意b就是一个普通的指针,类似于int* 不要混淆了,以为它是unique_ptr
b->reset(new int(23));
cout << *a << endl; // 23

unque_ptr引用

  • 使用引用并不会导致所有权的转移。只能通过reset()或是std::move()
  • 可以通过调用get(),获取裸指针,判断是否为nullptr来判断,所有权是否被转移
#include <iostream>
#include <memory>
#include <algorithm>
using namespace std;

int main(void){
	
	auto a = make_unique<int>(32);
	
	if(a.get() == nullptr){
		cout << "transfer1" <<endl;
	}
	// 使用引用,并不会转移所有权
	auto &b = a;
	if(a.get() == nullptr){
		cout << "transfer2" <<endl;	
	}
	
	// 将资源所权转移
	auto c(move(a));
	if(a.get() == nullptr){
		cout << "transfer3" <<endl;	// 这行最终输出
	}
	return 0;
}

reset方法

  • reset后会将原来指向的内存释放, 转为指向现在所给定的。
  • 不传参数会将对应内存提前释放。
#include <iostream>
#include <memory>
#include <algorithm>
using namespace std;

class test{
public:
	test(){}
	~test(){cout <<"see you" << endl;}
};

int main(void){
	
	auto t1 = make_unique<test>();

    // 
	t1.reset(new test());// 输出see you
	while(1){}//将程序卡住
	return 0;
}

mutex & shared_mutex

std::shared_mutex mutex_;
mutex_.lock();// 获取唯一锁,确保只有一个线程可以写入受保护的数据
mutex_.unlock();// 释放唯一锁

mutex_.lock_shared();// 获取共享锁,允许多个线程同时可以读取受保护的对象
mutex_.unlock_shared();// 释放共享锁

// 在具体的底层实现上,当有线程持有共享锁时,其它线程将写锁无法被获取。
// 同样的,当线程持有写锁时,其它线程将无法获取写锁或共享锁。

dynamic_cast

判断是子类还是父类,将某一指针转换为指定类型, 转换成功说明它本来就是这种类型,反正则不是,失败返回nullptr

// cur 是TrieNode*
// 使用dynamic_cast判断cur 是否是TrieNodeWithValue* 
if (dynamic_cast<TrieNodeWithValue<T> *>(cur) == nullptr) {
  *success = false;
  latch_.RUnlock();
  return T();
}


标签:字符,char,结尾,445,Project,C++,key,unique,节点
From: https://blog.51cto.com/u_15333750/6215451

相关文章

  • C++课本第四章例题
    个人银行账户管理程序1#include<iostream>2#include<cmath>3usingnamespacestd;4classSavingsAccount{//储蓄账户类5private:6intid;//账号7doublebalance;//余......
  • VC++ | DLL的创建和使用
    文章目录DLL的创建和使用动态链接库概述1.新建项目1-1.新建文件1-2.生成动态链接库2.Dumpbin命令2-1.用法3.从DLL中导出函数4.参考DLL的创建和使用动态链接库概述1.新建项目1-1.新建文件新建DLL1.cpp#include"pch.h"intadd(inta,intb){ return(a+b);}intsubtract(i......
  • C++的拓扑排序实现
    template<typenameT=CString,typename_Data=CString> structUnion_node//!<节点 { Union_node():nColor(0){} std::vector<Union_node*>vecNodeSon; Tkey;//!<关键数据 _Datadata;//!<卫星数据 mutableintnColor;//0:白色节点(未发现),1:灰色节点(发现),......
  • 初学者代码训练Day5(c/c++)
    打鱼还是晒网要求中国有句俗语叫“三天打鱼两天晒网”。某人从1990年1月1日起开始“三天打鱼两天晒网”,问这个人在以后的某一天中是“打鱼”还是“晒网”。流程图  代码1#include<iostream>2usingnamespacestd;34intmain()5{intyear=0,month=0,day=......
  • C++恶意软件开发(五)Linux shellcoding
    什么是shellcode?Shellcode通常指的是一段用于攻击的机器码(二进制代码),可以被注入到目标计算机中并在其中执行。Shellcode的目的是利用目标系统的漏洞或弱点,以获取系统控制权或执行恶意操作。它的名称来自于它经常被注入到攻击者编写的恶意软件的shell环境中,以便让攻击者可以更......
  • C语言和C++推荐书籍
    《CPrimerPlus》(第六版)作者:StephenPrata《C和指针》(第二版)作者:KennethA.Reek《C语言程序设计》(第四版)作者:谭浩强《C++Primer》(第五版)作者:Lippman,Lajoie,andMoo《EffectiveC++》(第三版)作者:ScottMeyers《STL源码剖析》作者:侯捷《深入理解C++11:C++11新特性解析与......
  • C语言和C++的switch语句用法
    C语言和C++的switch语句用法是相似的,但在一些细节上有所不同。在C语言中,switch语句的用法如下:switch(expression){  caseconstant1:    //dosomething    break;  caseconstant2:    //dosomething    break;  //...  ......
  • c++打卡第十二天
    一、问题描述。 二、设计思路①、我们可以从第五年往前推算,即1000=前一年剩余的钱*(1+12*0.0063),算出的结果加上一千就是前一年年初加上利息所得的总钱。②、列出五行式子就可以算出解。③、打印出程序运行结果。三、代码实现。#include<iostream>usingnamespacestd;i......
  • C++调用自定义源文件函数
    C++调用自定义源文件函数的步骤如下:在需要调用函数的源文件中包含自定义源文件的头文件。例如,如果需要调用名为myfunc.cpp的自定义源文件中的函数,则需要在调用该函数的源文件中包含myfunc.h头文件。编译自定义源文件。如果使用命令行编译,可以使用以下命令编译自定义源文件并生成......
  • 【c++】容器
    c++中容器的定义如下:数据存储上,有一种对象类型,它可以持有其他对象或指向其他对象的指针,这种对象类型叫容器。通俗的说容器就是保存其他对象的对象,这种“对象”还包含了一些列处理其他对象的方法,这也体现了容器类的一个好处,“容器类对特定代码重用问题的良好的解决方案”。容器另......