曾经沧海难为水,除却巫山不是云。|

Joey-Wang

园龄:4年3个月粉丝:17关注:0

2022-11-22 21:48阅读: 387评论: 0推荐: 0

# Project #0 - C++ Primer

https://15445.courses.cs.cmu.edu/fall2022/project0/

bustub 项目用 C++ 17 编写,但 C++ 11 已经够用。

C++ 相关教程:

关于GDB调试:

git 命令:

PROJECT SPECIFICATION

该项目中,实现一个由并发 Trie 支持的kv存储。Trie 是一种有效的 ordered-tree 数据结构,用于检索给定键的值。为了简化解释,我们假设键都是非空的可变长度字符串,但实际上它们可以是任意类型。

Trie 中的每个节点存储一个键的单个字符,并且可以有多个子节点,这些子节点表示不同的可能的下一个字符。当到达一个键的结尾时,将设置一个标志来指示其对应的节点是一个结束节点。

在下面的示例中,trie 存储键 HELLOHATHAVE

image-20221013195202758

你将实现的kv存储能存储 map 到任何类型的 value 的 string key。key 对应的 value 值存储在表示该 key 的最后一个字符的结点(又称 terminal node)中。

例如,考虑在 trie 中插入 kv 对(“ ab”,1)和(“ ac”,“ val”)。这两个键共享同一个父节点 “a”。在左边的子节点中,与键 “ab” 对应的值 1 存储在节点 “b” 中,与键 “ac” 对应的值 “val” 存储在节点 “c” 中。

IMPLEMENTATION

您只需要修改 BusTub 中的一个文件 p0_ trie.h (src/include/primer/p0 _ trie.h)。除了测试代码之外,您不需要修改存储库中的任何其他文件。

函数原型和成员变量在文件中指定。该项目要求您填写所有构造函数、析构函数和成员函数的实现。如果您认为合适,可以添加任何其他的辅助函数和成员变量,但不要修改现有的函数和变量。

Task #1 - Templated Trie

在这个头文件中,我们定义了三个必须实现的类。我们建议您首先实现 Trie 类的单线程版本,然后再转向并发版本。

TrieNode Class

TrieNode 类在 Trie 中定义单个节点。TrieNode 保存 key 的一个字符,is_end flag 指示它是否标记 key string 的结束。child nodes 的 key 字符从成员变量 children_ 这个map 访问得到,它存储charunique_ptr<TrieNode> 的映射。

注意:child node 为 unique_ptr,因此将它们分配给其他变量时需要小心。InsertChildNodeGetChildNode 都返回一个指向 unique_ptr 的指针,这样就可以在不制作 copy 或放弃所有权的情况下访问 unique_ptr 的数据。

最后,移动构造函数 TrieNode(TrieNode &&other_trie_node) 用于将旧 TrieNode 的 unique pointer 传输到新的 TrieNode。确保在传输数据时没有复制唯一指针。

TrieNodeWithValue Class

TrieNodeWithValue 类继承自 TrieNode,表示一个 terminal node,它的 key_char 是 key 的最后一个字符,该结点可保存任意类型 T的 value ,is_end_ flag 始终为 true。

当使用给定的 key 迭代一个 trie 树并到达最终字符时,你将根据不同场景调用 TrieNodeWithValue 的不同构造函数(详见 Trie Class 部分)。现在,你需要知道的是,TrieNodeWithValue (char key_char,T value) 构造函数根据给定的 key 的字符和 value 构建一个 TrieNodeWithValueTrieNodeWithValue (TrieNode &&trieNode,T value) 构造函数从给定的 trieNode 中获取 unique_ptr 的所有权,并将它自己的 value_ 设置为给定的值。

Trie Class

Trie 类定义支持 insert、search、remove 操作的实际 Trie 树。Trie 树的根节点是所有 key 的起始节点,它本身不应该存储任何键字符。

Insert

要在 Trie 树中插入一个kv,首先要使用给定的 key 遍历该 Trie 树,若不存在 TrieNode,则插入。⚠️ 不允许插入一个重复的 key,应该返回 false。一旦到达 key 的最后一个字符,会有三种情况:

  1. 具有该字符的 TrieNode 不存在。则需要调用TrieNodeWithValue (char key_char,T value) 构造函数来创建具有指定 key_char 和 value 的 trie node。利用多态性 —— TrieNode 类的 unique_ptr 也可存储 TrieNodeWithValue 类的 unique_ptr
  2. 具有该字符的 TrieNode 存在,但不是 terminal node(is_end_==false)。这意味着 unique_ptr 指向的是 TrieNode 类对象而不是 TrieNodeWithValue 类对象。需要调用 TrieNodeWithValue (TrieNode &&trieNode,T value) 构造函数将旧TrieNode 对象转为新的TrieNodeWithValue
  3. 具有该字符的 TrieNode 存在,且已是 terminal node(is_end_==true)这意味着 unique_ptr 已经指向一个 TrieNodeWithValue 类对象。应该立即返回 false,因为 key 不允许重复。

Remove

从 Trie 树中删除一个kv:

  1. 根据给定的 key 遍历该 Trie 树,若该 key 不存在,立即 return。
  2. 将 terminal node 的 is_end_ flag 变为 false。
  3. 若 terminal node 没有任何child结点,将它从父结点的children_ map 中移除。
  4. 遍历 Trie 树并递归删除没有子结点的结点,当遇到有子结点的结点时就停止。

GetValue

对于给定的 key,返回对应的 value。若找不到 key 或提供的 type 与结点 value 的 type 不匹配,则将 success 设为 false。要检查这两种 type 是否相同,dynamic_cast raw TrieNode pointer to TrieNodeWithValue<T> pointer. 若转换结果为 nullptr 则 type T 与结点存储的 value 的类型不匹配。

Task #2 - Concurrent Trie

Trie 树需要保证 insert、search、remove 操作在多线程环境中工作。可使用 RwLatch(BusTub 对 readers-writer lock 的实现)或 C++ STL 的std::shared_mutex 来实现。

这个项目只需要你通过获取根节点的 read/write lock 来实现简单的并发控制。

  • GetValue 函数应该从根结点获取一个 read lock(通过调用 RwLatchRLock 方法)
  • InsertRemove 操作应该从根结点获取一个 write lock(通过调用 RwLatchWLock 方法)

若使用 RWLatch,请确保在函数返回之前解锁所有获取的锁,以避免死锁。

Testing

可以使用我们的测试框架来测试此任务的各个组件。我们对单元测试用例使用 GTest。

test/primer/starter_trie_test.cpp 文件包含了对以上三个类的测试。

测试前要删掉测试文件中各函数前的 DISABLED_ 前缀。

$ cd build
$ make starter_trie_test
$ ./test/starter_trie_test

提交

您将把您的实现提交给 Grescope:https://www.gradescope.com/courses/424375/

您只需在提交的 zip 文件中包含以下文件及其完整路径:

  • src/include/primer/p0_trie.h

或者,在你的工作目录 (aka bustub, bustub-private, etc.) 运行👇 zip 命令,将创建一个名为 project0-submission.zip 的 zip 归档文件,你可以将它提交给 Gradescope。您还可以将此命令放在 bash 文件中,并运行 bash 文件,以使事情变得更容易。

$ zip project0-submission.zip \
    src/include/primer/p0_trie.h 

您可以使用 unzip 命令验证文件的内容。这个命令的输出应该如下所示:

$ unzip -l project0-submission.zip
Archive:  project0-submission.zip
 Length      Date    Time    Name
--------  ---------- -----   ----
    4465  2020-08-25 19:25   src/include/primer/p0_trie.h
--------                     -------
    4782                     1 files

代码实现

TrieNode Class

class TrieNode {
 public:
  explicit TrieNode(char key_char) : key_char_(key_char) {}

  TrieNode(TrieNode &&other_trie_node) noexcept
      : key_char_(other_trie_node.key_char_),
        is_end_(other_trie_node.is_end_),
        children_(std::move(other_trie_node.children_)) {}

  virtual ~TrieNode() = default;

  bool HasChild(char key_char) const {
    return children_.find(key_char) != children_.end();
  }

  bool HasChildren() const {
    return !children_.empty();
  }

  bool IsEndNode() const {
    return is_end_;
  }

  char GetKeyChar() const {
    return key_char_;
  }

  std::unique_ptr<TrieNode> *InsertChildNode(char key_char, std::unique_ptr<TrieNode> &&child) {
    if (HasChild(key_char) || child->GetKeyChar() != key_char) {
      return nullptr;
    }
    children_[key_char] = std::move(child);
    return &children_[key_char];
  }

  std::unique_ptr<TrieNode> *GetChildNode(char key_char) {
    if (!HasChild(key_char)) {
      return nullptr;
    }
    return &children_[key_char];
  }

  void RemoveChildNode(char key_char) {
    if (!HasChild(key_char)) {
      return;
    }
    children_.erase(key_char);
  }

  void SetEndNode(bool is_end) {
    is_end_ = is_end;
  }

 protected:
  char key_char_;
  bool is_end_{false};
  std::unordered_map<char, std::unique_ptr<TrieNode>> children_;
};

TrieNodeWithValue Class

template<typename T>
class TrieNodeWithValue : public TrieNode {
 private:
  T value_;

 public:
  TrieNodeWithValue(TrieNode &&trieNode, T value) : TrieNode(std::move(trieNode)) {
    value_ = value;
    is_end_ = true;
  }

  TrieNodeWithValue(char key_char, T value) : TrieNode(key_char) {
    value_ = value;
    is_end_ = true;
  }

  ~TrieNodeWithValue() override = default;

  T GetValue() const { return value_; }
};

Trie Class

class Trie {
 private:
  /* Root node of the trie */
  std::unique_ptr<TrieNode> root_;
  /* Read-write lock for the trie */
  ReaderWriterLatch latch_;

 public:
  /**
   * TODO(P0): Add implementation
   *
   * @brief Construct a new Trie object. Initialize the root node with '\0'
   * character.
   */
  Trie() {
    root_ = std::make_unique<TrieNode>('\0');
  }

  /**
   * TODO(P0): Add implementation
   *
   * @brief Insert key-value pair into the trie.
   *
   * If key is empty string, return false immediately.
   *
   * If key alreadys exists, return false. Duplicated keys are not allowed and
   * you should never overwrite value of an existing key.
   *
   * When you reach the ending character of a key:
   * 1. If TrieNode with this ending character does not exist, create new TrieNodeWithValue
   * and add it to parent node's children_ map.
   * 2. If the terminal node is a TrieNode, then convert it into TrieNodeWithValue by
   * invoking the appropriate constructor.
   * 3. If it is already a TrieNodeWithValue,
   * then insertion fails and return false. Do not overwrite existing data with new data.
   *
   * You can quickly check whether a TrieNode pointer holds TrieNode or TrieNodeWithValue
   * by checking the is_end_ flag. If is_end_ == false, then it points to TrieNode. If
   * is_end_ == true, it points to TrieNodeWithValue.
   *
   * @param key Key used to traverse the trie and find correct node
   * @param value Value to be inserted
   * @return True if insertion succeeds, false if key already exists
   */
  template<typename T>
  bool Insert(const std::string &key, T value) {
    if (key.empty()) {
      return false;
    }
    latch_.WLock();
    std::unique_ptr<TrieNode> *node = &root_;
    for (size_t i = 0; i < key.length() - 1; i++) {
      // 当前结点不存在 key[i] 对应的子结点
      if (node->get()->GetChildNode(key[i]) == nullptr) {
        std::unique_ptr<TrieNode> temp(new TrieNode(key[i]));
        node->get()->InsertChildNode(key[i], std::move(temp));
      }
      node = node->get()->GetChildNode(key[i]);

    }
    std::unique_ptr<TrieNode> *terminal_node = node->get()->GetChildNode(key[key.length() - 1]);
    // 不存在 terminal node
    if (terminal_node == nullptr) {
      std::unique_ptr<TrieNodeWithValue<T>> temp(new TrieNodeWithValue(key[key.length() - 1], value));
      node->get()->InsertChildNode(key[key.length() - 1], std::move(temp));
      latch_.WUnlock();
      return true;
    }
    // terminal node is a TrieNode
    if (!terminal_node->get()->IsEndNode()) {
      TrieNodeWithValue<T> *temp = new TrieNodeWithValue(std::move(**terminal_node), value);
      terminal_node->reset(temp);
      latch_.WUnlock();
      return true;
    }
    // already a TrieNodeWithValue
    latch_.WUnlock();
    return false;
  }

  /**
   * TODO(P0): Add implementation
   *
   * @brief Remove key value pair from the trie.
   * This function should also remove nodes that are no longer part of another
   * key. If key is empty or not found, return false.
   *
   * You should:
   * 1) Find the terminal node for the given key.
   * 2) If this terminal node does not have any children, remove it from its
   * parent's children_ map.
   * 3) Recursively remove nodes that have no children and is not terminal node
   * of another key.
   *
   * @param key Key used to traverse the trie and find correct node
   * @return True if key exists and is removed, false otherwise
   */
  bool Remove(const std::string &key) {
    if (key.empty()) {
      return false;
    }
    latch_.WLock();
    std::vector<std::unique_ptr<TrieNode> *> pre_nodes;
    std::unique_ptr<TrieNode> *node = &root_;
    for (char i : key) {
      pre_nodes.emplace_back(node);
      node = node->get()->GetChildNode(i);
      if (node == nullptr) {
        latch_.WUnlock();
        return false;
      }
    }
    node->get()->SetEndNode(false); // 变为中间节点
    for (int i = pre_nodes.size() - 1; i >= 0; --i) {
      std::unique_ptr<TrieNode> *child = pre_nodes[i]->get()->GetChildNode(key[i]);
      // child 是中间节点且没有子结点,就删除该 child
      // !! 一定要判断child是不是中间节点,因为terminal node本身就没有子结点
      if (!child->get()->HasChildren() && !child->get()->IsEndNode()) {
        pre_nodes[i]->get()->RemoveChildNode(key[i]);
      } else {
        break;
      }
    }
    latch_.WUnlock();
    return true;
  }

  /**
   * TODO(P0): Add implementation
   *
   * @brief Get the corresponding value of type T given its key.
   * If key is empty, set success to false.
   * If key does not exist in trie, set success to false.
   * If given type T is not the same as the value type stored in TrieNodeWithValue
   * (ie. GetValue<int> is called but terminal node holds std::string),
   * set success to false.
   *
   * To check whether the two types are the same, dynamic_cast
   * the terminal TrieNode to TrieNodeWithValue<T>. If the casted result
   * is not nullptr, then type T is the correct type.
   *
   * @param key Key used to traverse the trie and find correct node
   * @param success Whether GetValue is successful or not
   * @return Value of type T if type matches
   */
  template<typename T>
  T GetValue(const std::string &key, bool *success) {
    *success = false;
    if (key.empty()) {
      return {};
    }
    latch_.RLock();
    std::unique_ptr<TrieNode> *node = &root_;
    for (char i : key) {
      node = node->get()->GetChildNode(i);
      if (node == nullptr) {
        latch_.RUnlock();
        return {};
      }
    }
    T res;
    auto terminal_node = dynamic_cast<TrieNodeWithValue<T> *>(node->get());
    if (terminal_node == nullptr) {
      res = {};
    } else {
      *success = true;
      res = terminal_node->GetValue();
    }
    latch_.RUnlock();
    return res;
  }
};

出现的问题

1️⃣ 内存泄露 for (size_t i = N; i >= 0; i--)

因为 size_t 是无符号的,因此当i = 0 时 i-- 永远不会小于零。如果限制写 i>0 还是不会有内存泄露的。

解决:还是使用 for (int i = N; i >= 0; i--)

2️⃣ Trie 的 Remove(const std::string &key) 方法中,要判断child结点为中间节点且没有children才能 RemoveChildNode。因为可能存在这种情况:

bool success = trie.Insert<int>("a", 5);
EXPECT_EQ(success, true);
success = trie.Insert<int>("aa", 6);
EXPECT_EQ(success, true);
success = trie.Insert<int>("aaa", 7);
EXPECT_EQ(success, true);

success = trie.Remove("aaa");
EXPECT_EQ(success, true);

root
|
aa->5
|
aa->6
|
aaa->7

此时我们将 terminal node 变为中间结点(is_end_=false)然后回溯进行删除子结点操作:

  • aa->6 存在一个child结点 aaa->7,该结点是中间结点且无 children,因此删除该child结点
  • aa->5 存在一个child结点 aa->6,该结点虽然无 children,但是 terminal node,因此不用删除
    (⚠️若此时不判断是不是中间结点,只判断该结点有无children,就会误删该结点)

测试提交结果

测试的时候要删掉test/primer/starter_trie_test.cpp 测试文件中各函数前的 DISABLED_ 前缀。

$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Debug ..
$ make -j4
$ make starter_trie_test
$ ./test/starter_trie_test
image-20221116160641488
# 运行代码格式化和规范检查,必须改正所有提示,否则后续提交评分为 0
$ make format
$ make check-lint
$ make check-clang-tidy-p0

# 到达工作目录bustub
$ cd ../  
$ zip project0-submission.zip \
    src/include/primer/p0_trie.h 

# 验证压缩文件内容
$ unzip -l project0-submission.zip
Archive:  project0-submission.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
    13097  11-16-2022 16:14   src/include/primer/p0_trie.h
---------                     -------
    13097                     1 file
image-20221116165803105

本文作者:Joey-Wang

本文链接:https://www.cnblogs.com/joey-wang/p/16916598.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Joey-Wang  阅读(387)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开