cmu15445 2023fall project0 C++ prime

课程主页(2023)

trie 字典树 在写入时复制。

首先把成员理清。在对所给项目进行实现。

一.需要具备的基础知识:

1.智能指针:shared_ptr.作用如同指针,但会记录有多少个shared_ptr共同指向一个对象。这便是所谓的引用计数(reference counting)。
一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。

创建时利用make_shared<>.例如

std::shared_ptr<T> val_p = std::make_shared<T>(std::move(value));

2.move()函数

将对象的状态或所有权转换给另一对象,之后传入的对象就为空了,这样做只是转移,没有内存的搬迁或者内存拷贝,如果不加则需要先将传入对象拷贝一份为临时对象,然后给属性成员赋值,之后销毁临时对象。因此,通过std::move(),可以避免不必要的拷贝操作。这样的构造函数称为移动构造函数

3.auto 函数() -> 类型{…} 作用是推导出函数的返回类型

auto Trie::Put(std::string_view key, T value) const -> Trie {

如此,put()函数返回的就是Trie对象。

4.类型转换

std::dynamic_cast<目标类型*>(被转换对象)
std::dynamic_pointer_cast<目标类型>(被转换智能指针)
dynamic_pointer_cast与dynamic_cast的区别
适用对象类型:dynamic_pointer_cast适用于智能指针类型,如std::shared_ptr和std::weak_ptr,用于进行智能指针的动态类型转换。而dynamic_cast适用于指针和引用类型,用于进行指针或引用类型的动态类型转换。

const auto *value_node = dynamic_cast<const TrieNodeWithValue<T> *>(node.get());

在Get()函数中用到了类型转换。

二.具体的实现

在Get()函数中,所给要求根据传入的Key参数,从根节点开始遍历字符串,若寻找到则返回value;

auto Trie::Get(std::string_view key) const -> const T * {
  // throw NotImplementedException("Trie::Get is not implemented.");
  //  从根节点开始
  auto node = root_;
  // 按输入的key值遍历Trie树
  for (char ch : key) {
    // 如果当前节点为nullptr,或者没有对应字符的子节点,返回nullptr,程序结束
    if (node == nullptr || node->children_.find(ch) == node->children_.end()) {
      return nullptr;
    }
    // 当前字符存在,接着往后面找
    node = node->children_.at(ch);
  }
  // 路径存在,检查类型是否匹配
  // 将节点转换为TrieNodeWithValue<T>类型
  const auto *value_node = dynamic_cast<const TrieNodeWithValue<T> *>(node.get());
  // 如果转换成功并且节点包含正确类型的值,则返回该值的指针
  if (value_node != nullptr) {
    return value_node->value_.get();
  }
  // 若类型不匹配,返回nullptr
  return nullptr;

在Put()函数中,为键设置相应的值。如果密钥已存在,则覆盖现有值。请注意,该值的类型可能是不可复制的(即 )。此方法返回一个新的 trie。

分类讨论。

第一种情况:当值需要放在根节点中时,有跟的时候直接将值放入,没有根时创建根再放入值。

此时要注意,因为是要写入时复制,所以要分析原根有无子节点。要是原根没有子节点,则直接传入值,要是有子节点,则需要把原来根的子节点复制给新的根。

第二种情况:当值不放在根节点时

如果此时没有根,则新建一棵空树,如果有根节点,则递归的插入值。

下面介绍具体的递归的插入算法:

确认递归的出口,key结束。每一次插入都是递归开始推出时插入。

设置一个bool参数flag,分类,

如果key的值匹配路径flag设为true。如果此时剩余的Key大于1,则需要继续递归向下找(当还要继续向下找的时候,必须要先复制当前节点,再向下找。)如果此时剩余的Key小于等于1,则直接插入。

如果在当前节点的子节点没有找到匹配的Key,则新建一个节点。判断key的大小,如果是最后一个key,则创建一个带值节点,否则创一个children节点,在递归的向下寻找。

void PutCycle(const std::shared_ptr<bustub::TrieNode> &new_root, std::string_view key, T value) {
  // 判断元素是否为空的标志位
  bool flag = false;
  // 在new_root的children找key的第一个元素
  // 利用for(auto &a:b)循环体中修改a,b中对应内容也会修改及pair的特性
  for (auto &pair : new_root->children_) {
    // 如果找到了
    if (key.at(0) == pair.first) {
      flag = true;
      // 剩余键长度大于1
      if (key.size() > 1) {
        // 复制一份找到的子节点,然后递归对其写入
        std::shared_ptr<TrieNode> ptr = pair.second->Clone();
        // 递归写入 .substr(1,key.size()-1)也可以
        // 主要功能是复制子字符串,要求从指定位置开始,并具有指定的长度。
        PutCycle<T>(ptr, key.substr(1), std::move(value));
        // 覆盖原本的子节点,必须重新让pair指针(引用)指向新创建的节点,否则下次用的还是没有复制前的节点
        pair.second = std::shared_ptr<const TrieNode>(ptr);
      } else {
        // 剩余键长度小于等于1,则直接插入
        // 创建新的带value的子节点
        std::shared_ptr<T> val_p = std::make_shared<T>(std::move(value));
        TrieNodeWithValue node_with_val(pair.second->children_, val_p);
        // 覆盖原本的子节点
        pair.second = std::make_shared<const TrieNodeWithValue<T>>(node_with_val);
      }
      return;
    }
  }
  if (!flag) {
    // 没找到,则新建一个子节点
    char c = key.at(0);
    // 如果为键的最后一个元素
    if (key.size() == 1) {
      // 直接插入children
      std::shared_ptr<T> val_p = std::make_shared<T>(std::move(value));
      new_root->children_.insert({c, std::make_shared<const TrieNodeWithValue<T>>(val_p)});
    } else {
      // 创建一个空的children节点
      auto ptr = std::make_shared<TrieNode>();
      // 递归
      PutCycle<T>(ptr, key.substr(1), std::move(value));
      // 插入
      new_root->children_.insert({c, std::move(ptr)});
    }
  }
}
template <class T>
auto Trie::Put(std::string_view key, T value) const -> Trie {
  // Note that `T` might be a non-copyable type. Always use `std::move` when creating `shared_ptr` on that value.
  // throw NotImplementedException("Trie::Put is not implemented.");
  // 返回值类型是一个Trie的对象

  // 第一种情况:值要放在根节点中,无根造根,有根放值
  // 更改根节点的值,即测试中的 trie = trie.Put<uint32_t>("", 123);key为空
  if (key.empty()) {
    std::shared_ptr<T> val_p = std::make_shared<T>(std::move(value));
    // 建立新根
    std::unique_ptr<TrieNodeWithValue<T>> new_root = nullptr;
    // 如果原根节点无子节点
    if (root_->children_.empty()) {
      // 直接修改根节点
      new_root = std::make_unique<TrieNodeWithValue<T>>(std::move(val_p));
    } else {
      // 如果有,把原根的关系转移给新根:root_的children改为newRoot的children
      // 这里看tire.h里TireNodeWithValue方法,可以看到,传入不同数量的参数,对应实现不同的方法。
      new_root = std::make_unique<TrieNodeWithValue<T>>(root_->children_, std::move(val_p));
    }
    // 返回新的Trie
    return Trie(std::move(new_root));
  }
  // 第二种情况:值不放在根节点中
  // 2.1 根节点如果为空,新建一个空的TrieNode;
  // 2.2 如果不为空,调用clone方法复制根节点
  std::shared_ptr<TrieNode> new_root = nullptr;
  if (root_ == nullptr) {
    new_root = std::make_unique<TrieNode>();
  } else {
    new_root = root_->Clone();
  }
  // 递归插入,传递根节点,要放的路径:key, 要放入的值:value
  PutCycle<T>(new_root, key, std::move(value));
  // 返回新的Trie
  return Trie(std::move(new_root));
  // You should walk through the trie and create new nodes if necessary. If the node corresponding to the key already
  // exists, you should create a new `TrieNodeWithValue`.
}

在Remove()函数中,根据key的值,删除value。此方法返回一个新的 trie。

先判断根是否为空,判断根节点是否有值,判断根节点是否有子节点,根节点有子节点,把子节点转移给新的根。如果键位不为空,创建新的根节点的副本,递归的删除。

具体的实现代码

// 返回表示是否删除了值
bool RemoveCycle(const std::shared_ptr<bustub::TrieNode> &new_roottry, std::string_view key) {
  // 先找第一个key元素是否匹配
  for (auto &pair : new_roottry->children_) {
    if (key.at(0) != pair.first) {
      // 不匹配就contiune
      continue;
    }
    // 如果是键尾
    if (key.size() == 1) {
      // 判断是否为带值节点
      if (!pair.second->is_value_node_) {
        // 不带值返回false,表示没有删除值
        return false;
      }
      // 如果带值但是没有子节点
      if (pair.second->children_.empty()) {
        // 直接删除
        new_roottry->children_.erase(pair.first);
      } else {
        // 带值但是有子节点,转化为不带值节点
        // 不能直接删除节点,而是 新建一个节点,把它原先的子节点重新传给他
        pair.second = std::make_shared<const TrieNode>(pair.second->children_);
      }
      return true;
    }
    // 如果匹配了key,但不是键尾
    // 拷贝当前节点
    std::shared_ptr<TrieNode> ptr = pair.second->Clone();
    bool flag = RemoveCycle(ptr, key.substr(1, key.size() - 1));
    // 递归结束没有要删除的键
    if (!flag) {
      return false;
    }
    // 如果删除后当前节点没有值也没有子节点
    // 则删除原先的当前节点
    if (!ptr->is_value_node_ && ptr->children_.empty()) {
      new_roottry->children_.erase(pair.first);
    } else {
      // 覆盖掉Ptr
      pair.second = std::shared_ptr<TrieNode>(ptr);
    }
    return true;
  }
  // key不匹配
  return false;
}

auto Trie::Remove(std::string_view key) const -> Trie {
  // throw NotImplementedException("Trie::Remove is not implemented.");
  if (this->root_ == nullptr) {
    return *this;
  }
  // 如果键位空
  if (key.empty()) {              // 根节点有值
    if (root_->is_value_node_) {  // 根节点没有字节点
      if (root_->children_.empty()) {
        return Trie(nullptr);
      }
      // 根节点有子节点
      // 根节点有子节点,把子结点转给新根
      std::shared_ptr<TrieNode> new_root = std::make_shared<TrieNode>(root_->children_);
      return Trie(new_root);
    }
    // 根节点无value,直接返回
    return *this;
  }
  // 创建一个当前根节点的副本作为新的根节点
  std::shared_ptr<TrieNode> newroot = root_->Clone();
  // 递归删除
  bool flag = RemoveCycle(newroot, key);
  if (!flag) {
    return *this;
  }
  if (newroot->children_.empty() && !newroot->is_value_node_) {
    newroot = nullptr;
  }
  return Trie(std::move(newroot));

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
CMU 15445 课程的 Project 0 是一个调试练习,旨在帮助学生熟悉调试工具和技术。在这个项目中,你将开始使用 GDB 和 Valgrind 这两个常用的调试工具,以及一些其他辅助工具。以下是一些问题和步骤,帮助你完成这个练习: 1. 你需要查看项目中提供的代码,并了解它的结构和功能,这样你才能更好地理解程序的逻辑和可能出现的 bug。 2. 接下来,你需要编译项目,并确保没有编译错误。如果出现错误,你需要修复它们,这可能需要检查一些语法错误或缺失的库。 3. 一旦成功编译项目,你就可以使用 GDB 进行调试了。GDB 是一个强大的调试器,可以帮助你找出程序中的错误。你可以使用 GDB 来单步执行代码、设置断点、查看变量的值等等。通过使用 GDB,你可以逐步查看代码运行的路径,并找出程序崩溃或产生错误的原因。 4. 在使用 GDB 进行调试时,你可以通过设置断点来暂停程序的执行,并查看变量的值和程序的状态。你可以使用“break”命令在程序中设置断点,并通过“run”命令启动程序。当程序到达这个断点时,它会停止执行,你可以使用“print”命令查看变量的值,或者“step”命令逐步执行代码。 5. 另一个常用的调试工具是 Valgrind。Valgrind 可以帮助你检测内存泄漏和错误的访问方式。你可以使用“valgrind”命令来运行程序,并查看 Valgrind 的输出。它会告诉你有关程序中任何潜在问题的信息,例如未初始化的变量、访问越界等。 6. 最后,当你发现 bug 并修复它们后,可以运行各种测试用例来验证程序的正确性。测试用例可以帮助你确定程序是否按预期工作,并且在修改代码后,它们可以帮助你确保你的修复没有引入新的错误。 通过完成 CMU 15445 项目 0 的调试练习,你将掌握一些重要的调试技巧和工具,这对于进一步开发和调试软件应用程序将非常有用。希望上述步骤和建议对你有所帮助,祝你顺利完成这个项目!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值