目录
前言
15445通关记录贴-project0
Task #1 - Copy-On-Write Trie
先写put,再写get和remove
这个任务的代码是用test/trie_test.cpp进行测试的,目前所有测试用例都可以通过
put方法
我是写的非递归版本。首先要单独处理key==""这种情况。key不为空时,遍历key的每个字符,当key[i]在curr->children的时候,就对curr->children[key[i]]进行复制,不在的时候就新建一个节点。然后将复制/新建的节点连接到curr->children[key[i]]。循环遍历直到i=key.size()-1,此时需要单独处理最后一个字符,因为最后一个字符对应的节点必须是带值的节点。如果最后一个字符所对应的节点已经存在,那么根据这个节点的children_和函数参数value创建一个值节点,否则只根据value创建值节点而不需要children_,然后将新节点连接到curr上
auto Trie::Put(std::string_view key, T value) const -> Trie {
// 由于value可能是不可拷贝的,所以需要使用move函数转移资源的控制权
auto vptr = std::make_shared<T>(std::move(value));
// 先处理key==""
if(key==""){
if(root_) return Trie(std::make_shared<TrieNodeWithValue<T>>(root_->children_,vptr));
else return Trie(std::make_shared<TrieNodeWithValue<T>>(vptr));;
}
// 根为空时先创建一个根
std::shared_ptr<TrieNode> root;
if(root_) root = std::move(root_->Clone());
else root = std::make_shared<TrieNode>();
// 遍历的过程需要不断复制遍历到的节点,如果一个节点的孩子中没有key[i],那就创建新的节点
auto curr = root;
for(size_t i=0; i<key.size(); i++){
auto it = curr->children_.find(key[i]);
auto p = std::make_shared<TrieNode>();
if(i != key.size()-1){// 非最后一个字符
if(it != curr->children_.end()) //存在则复制
p = it -> second -> Clone();
}
else{ // 对于最后一个字符,要插入一个带值的节点,无论这个节点是否存在都要创建一个新的值节点
if(it != curr->children_.end())
p = std::make_shared<TrieNodeWithValue<T>>(it->second->children_,vptr);
else
p = std::make_shared<TrieNodeWithValue<T>>(vptr);
}
curr->children_[key[i]] = p;
curr = p;
}
return Trie(root);
}
get方法
get方法很简单。根据key[i]遍历trie,如果某个节点不存在则直接返回null,如果一直遍历到最后一个字符都有节点存在,那么要判断最后一个节点是否是值节点,如果不是则返回null,如果是则返回节点的value。这里涉及到指针的动态转换,有两种写法。一种是在智能指针之间进行转换,一种是在裸指针之间进行转换
auto Trie::Get(std::string_view key) const -> const T * {
// 根为空
if(root_==nullptr)
return nullptr;
// 根非空
auto curr = root_;
size_t i =0;
while(i < key.size()){
auto it = curr->children_.find(key[i]);
if(it != curr->children_.end())
curr = it->second;
else
return nullptr;
i++;
}
// 写法1
// std::shared_ptr<const TrieNodeWithValue<T>> p = std::dynamic_pointer_cast<const TrieNodeWithValue<T> >(curr);
// if(p)
// return p->value_.get();
// return nullptr;
// 写法2
const TrieNodeWithValue<T>* node = dynamic_cast<const TrieNodeWithValue<T>*>(curr.get());
if (node)
return node->value_.get();
return nullptr;
}
remove方法
非递归版本,遍历的过程中要用栈保存遍历的路径。之后通过栈一个个删除孩子数为0且不含值的节点
auto Trie::Remove(std::string_view key) const -> Trie {
// 根为空
if(root_==nullptr)
return Trie();
// 键为空串
if(key==""){
auto root = std::make_shared<TrieNode>(root_->children_);
if (root -> children_.size() == 0)
return Trie();
else
return Trie(root);
}
// 正常情况,用栈保存路径以便于删除
std::stack<std::shared_ptr<TrieNode>> node_stack;
std::stack<char> char_stack;
std::shared_ptr<TrieNode> root = std::move(root_->Clone());
auto curr = root;
node_stack.push(root);
// 遍历以定位要删除的节点
for (std::size_t i = 0; i < key.size(); ++i) {
auto it = curr->children_.find(key[i]);
std::shared_ptr<TrieNode> p;
if(it != curr->children_.end()){
if(i == key.size() - 1)
// 最后一个字符对应的节点必须转成TrieNode,以表示他对应的key被删除了,这里就需要创建新节点
p = std::make_shared<TrieNode>(it -> second -> children_);
else
// 非最后一个字符对应的节点仍需遵循写时复制的原则,需要拷贝
p = it -> second -> Clone();
curr->children_[key[i]] = p;
curr = p;
node_stack.push(p);
char_stack.push(key[i]);
}
else
// 某个字符对应的节点不存在,说明key不存在,不需要删除任何节点
return Trie(root);
}
// 只要能走到这里,说明待删除的节点是肯定存在的,后续就是不断删除从这个节点往根节点的路径上的孩子数为0且不含值的节点
// 从待删除节点开始,不断删除孩子数为0且不含值的节点,直到遇到孩子数不为0或含值的节点则停止
auto sub = node_stack.top();
node_stack.pop();
while(node_stack.size() != 0){
auto parent = node_stack.top();
if(sub->children_.size() == 0 && sub->is_value_node_ == false)
parent->children_.erase(char_stack.top());
else
break;
node_stack.pop();
char_stack.pop();
sub = parent;
}
// 根可能也是需要删除的,上面删除节点的循环实际上没办法处理根节点,因为根节点没有父节点,所以需要单独处理一下根节点
if(root->children_.size()==0 && sub->is_value_node_ == false)
return Trie();
return Trie(root);
}
Task #2 - Concurrent Key-Value Store
这里需要学一下C++里面跟锁有关的东西,std::mutex是一种互斥量,针对它实现的锁只有两种std::lock_guard、std::unique_lock。他们都是对互斥量上锁以实现互斥,但是std::lock_guard没法手动解锁,std::unique_lock可以调用unlock()手动解锁,std::unique_lock的粒度更细。
按我的理解,我以为要用std::shared_lock和std::unique::lock来实现读写锁(读与读可以并发,读与写互斥,读与读互斥),结果发现std::shared_lock只能对std::shared_mutex上锁,不能用于std::mutex。
可以看下这篇博客对c++锁的介绍
C++锁的一些介绍
auto TrieStore::Get(std::string_view key) -> std::optional<ValueGuard<T>> {
std::unique_lock<std::mutex> rlock(root_lock_);
auto root = root_;
rlock.unlock();
auto value = root.Get<T>(key);
if(value)
return ValueGuard<T>(root,*value);
return std::nullopt;
}
template <class T>
void TrieStore::Put(std::string_view key, T value) {
std::unique_lock<std::mutex> wlock(this->write_lock_);
const Trie trie = this->root_.Put<T>(key, std::move(value));
std::unique_lock<std::mutex> rlock(root_lock_);
root_ = trie;
rlock.unlock();
wlock.unlock();
}
void TrieStore::Remove(std::string_view key) {
std::unique_lock<std::mutex> wlock(this->write_lock_);
const Trie trie = this->root_.Remove(key);
std::unique_lock<std::mutex> rlock(root_lock_);
root_ = trie;
rlock.unlock();
wlock.unlock();
}
Task #3 - Debugging
第一问:调试可以看到root有9个孩子
但是这里gdb没办法显示更深层的节点,比如我想display trie.root_->children_.at(‘9’) 这样查看root的第九个子节点,会报错:
trie.root_->children_.at(‘9’) = <error: Cannot evaluate function – may be inlined>
也就是说它没法查看更深层的节点了,所以直接上输出语句吧,我想这里应该是有办法用gdb查看深层节点的,但是我懒得去查资料了
第二问和第三问:
答案分别是1和25
Task #4 - SQL String Functions
string_expression.h在src/include/execution/expressions下
plan_func_call.cpp在src/planner下
单论字符串的大小写转换的代码当然是很简单的,但是说实话我没看懂这里的的函数调用关系以及这里的这些类,感觉很复杂,不知道这里的代码该咋写,因此直接参考了别人的代码(太菜了):
参考博文