C++【一棵红黑树封装 set 和 map】

简介: C++【一棵红黑树封装 set 和 map】

🌇前言

红黑树的基本情况我们已经在上一篇文章中学习过了,本文主要研究的是红黑树的实际应用:封装实现 setmap,看看如何通过一棵红黑树满足两个不同的数据结构;在正式封装之前,先要对之前的红黑树进行完善,增加必要功能


🏙️正文

1、红黑树的完善

1.1、修改默认成员函数

红黑树 中的每个节点都可能开辟独立的内存空间,因此在涉及拷贝、赋值等操作时,默认生成的成员函数已经无法满足需求了 --> 会导致两个指针指向同一块空间,然后造成重复析构问题

所以我们需要对其中的 默认成员函数 进行改造,手动添加符合要求的 默认成员函数

1.1.1、默认构造

写出默认构造函数是为了后面的 拷贝构造 做准备,因为祖师爷规定:只要我们写了构造函数(比如拷贝构造),就需要提供一个不需要传递参数的默认构造函数,否则编译会报错

假设只写了 拷贝构造 函数,编译时会报错:

所以需要提供一个 默认构造函数

//因为写了拷贝构造,所以需要有默认构造
RBTree()
  :_root(nullptr)
{}

注意:默认构造函数的要求是:不需要传递参数的构造函数,所以全缺省的拷贝构造函数也行,但最好还是额外提供一个无参版本

1.1.2、析构 —> 遍历释放

红黑树 中的节点可能涉及 动态内存申请,而编译器生成的 析构函数 无法满足 红黑树 的需求:释放其中的每个节点,所以我们需要编写 析构函数,释放其中的每个节点,确保不会出现 内存泄漏 问题

释放思路: 借助 后序遍历 -> 左右根 的思想,遍历到每一个不为空的节点,然后释放即可

因为需要 递归释放,所以推荐将释放流程封装为单独的函数,方便进行递归,析构函数 直接调用即可

//析构
  ~RBTree()
  {
    _destroy(_root);
  }
protected:
  void _destroy(Node*& root)
  {
    if (root == nullptr)
      return;
    //后序遍历
    _destroy(root->_left);
    _destroy(root->_right);
    //销毁节点
    free(root);
    root = nullptr;
  }


细节:_destroy 中参数使用引用,可以在不同栈帧中置空同一个指针变量

1.1.3、拷贝构造 —> 深拷贝

编译器生成的 拷贝构造浅拷贝,当 红黑树 中的节点涉及动态内存申请时,程序运行必然会崩溃(多个指针指向同一块空间,导致重复析构

比如下图中的场景,就是使用了 编译器生成的拷贝构造函数(浅拷贝)

void RBTreeTest1()
{
  RBTree<int, string> rb1;
  rb1.Insert(make_pair(1, "a"));
  RBTree<int, string> rb2(rb1); //rb2 拷贝构造 rb1
}


此时就需要手动实现 拷贝构造(深拷贝)

深拷贝思路:照着被拷贝红黑树,逐个节点申请空间,并进行链接即可

类似于 根据中序、后序重构二叉树的思想

//拷贝构造
  RBTree(const RBTree<K, V>& tree)
    :_root(nullptr)
  {
    //深拷贝 ---> 遍历构造每个节点
    _root = _copy(tree._root);
  }
protected:
  Node* _copy(const Node* root)
  {
    if (root == nullptr)
      return nullptr;
    //构造新节点
    Node* new_node = new Node(root);
    //递归链接左右子树
    new_node->_left = _copy(root->_left);
    new_node->_right = _copy(root->_right);
    //注意父亲链的链接
    if (new_node->_left != nullptr)
      new_node->_left->_parent = new_node;
    if (new_node->_right != nullptr)
      new_node->_right->_parent = new_node;
    return new_node;
  }
• 29


借助 后序遍历 的思想重构好每个节点后,返给父亲进行链接,当整棵树都重构完成后,返回 根节点

注意:

  • 拷贝构造函数中的参数需要使用 引用,避免 无穷递归问题
  • 因为是三叉链结构,需要注意父指针的链接,判断不为空后直接链接即可
1.1.4、赋值重载

编译器生成的 赋值重载 函数也是 浅拷贝,实现 赋值重载 就比较简单了,有以下两种办法:

  1. 像 拷贝构造 一样,递归创建每一个节点
  2. 现代写法:直接与临时变量交换根节点

现代写法很简单,也更安全(只要 拷贝构造 没问题,那么现代写法也没问题),下面就是现代写法:

//赋值重载
RBTree<K, V>& operator=(RBTree<K, V> tmp)
{
  //直接交换根节点即可
  std::swap(_root, tmp._root);
  return *this;
}


tmp临时变量,传递参数时,会自动进行一次 拷贝构造 函数的调用,生成临时对象,并且此时是 深拷贝

临时变量 的资源不利用就浪费了,所以可以直接把它的 根节点 偷过来,间接完成了 红黑树 的赋值,原 红黑树 中的节点在函数运行后、临时变量 销毁时进行逐一释放(自动调用 析构函数

注意:现代写法中的参数不能使用引用,否则会导致被赋值的红黑树节点丢失

1.2、新增迭代器

红黑树 中也有 迭代器,因为是 链式 结构,所以在进行 迭代器 设计时,需要单独设计一个 迭代器类,就像 list 一样

1.2.1、整体设计

红黑树 的节点再一次封装,构建一个单独的 迭代器

因为此时节点的模板参数有 KV,所以 迭代器类 中也需要这两个参数

至于 迭代器类 设计时的精髓:不同类型的迭代器传递不同的参数 这里就不再展开叙述,简单来说,额外增加 RefPtr 的目的是为了让 普通迭代器const迭代器 能使用同一个 迭代器类

迭代器类中的多参数默认设计思想详见 《C++ STL学习之【list的模拟实现】

迭代器类 的大体框架如下:

//迭代器类
template<class K, class V, class Ref, class Ptr>
class __RBTreeIterator
{
  typedef RBTreeNode<K, V> Node;  //节点
  typedef __RBTreeIterator<K, V, Ref, Ptr> Self;  //迭代器
public:
  __RBTreeIterator()
    :_node(nullptr)
  {}
  //将节点构造为迭代器对象
  __RBTreeIterator(Node* root)
    :_node(root)
  {}
private:
  Node* _node;
};


其中的 RefPtr 具体是什么类型,取决于调用方传递了什么

1.2.2、移动操作

迭代器 最重要的操作莫过于 移动红黑树 的迭代器是一个 双向迭代器,只支持 ++-- 操作

树形 结构的容器在进行遍历时,默认按 中序遍历 的顺序进行迭代器移动,因为这样遍历 二叉搜索树 后,结果为 有序

清楚遍历路径后,就可以设计具体操作了

正向移动 operator++()operator++(int)

正向移动思路:

  1. 判断当前节点的右子树是否存在,如果存在,则移动至右子树中的最左节点
  2. 如果不存在,则移动至当前路径中 孩子节点为左孩子的父亲节点
  3. 如果父亲为空,则下一个节点就是空
//前置++
Self operator++()
{
  //左根右
  //思路:如果右子树存在,访问右子树的最左节点;如果右子树不存在,访问父亲
  //注意:避免 _node 为空
  if (_node != nullptr && _node->_right != nullptr)
  {
    //访问右子树的最左节点
    Node* cur = _node->_right;
    while (cur->_left != nullptr)
      cur = cur->_left;
    _node = cur;
  }
  else if(_node != nullptr)
  {
    //访问父亲节点(cur 须位于父亲的左边)
    Node*  cur = _node;
    Node* parent = _node->_parent;
    while (parent && parent->_left != cur)
    {
      cur = parent;
      parent = cur->_parent;
    }
    _node = parent;
  }
  return *this;
}
//后置++
Self operator++(int)
{
  Self tmp = *this;
  ++(*this);
  return tmp;
}


为什么右子树不为空时,要访问 右子树的最左节点

  • 因为此时是正向移动,路径为 左根右,如果右边路径存在,就要从它的最左节点开始访问

为什么右子树为空时,要访问当前路径中 孩子节点为左孩子 的父亲节点

  • 因为 孩子节点为右孩子 的父亲节点已经被访问过了

在这两种情况的组合之下,就可以完成 迭代器的正向移动

反向移动 operator--()operator--(int)

反向移动很简单,就是与正向相反即可

反向移动思路:

  1. 判断当前节点的左子树是否存在,如果存在,则移动至左子树中的最右节点
  2. 如果不存在,则移动至当前路径中 孩子节点为右孩子的父亲节点
  3. 如果父亲为空,则下一个节点就是空
//前置--
Self operator--()
{
  //右根左
  //思路:如果左子树存在,访问左子树的最右节点;如果左子树不存在,访问父亲
  //注意:避免 _node 为空
  if (_node != nullptr && _node->_left != nullptr)
  {
    //访问左子树的最右节点
    Node* cur = _node->_left;
    while (cur->_right != nullptr)
      cur = cur->_right;
    _node = cur;
  }
  else if(_node != nullptr)
  {
    //访问父亲节点(cur 必须置于父亲的右边)
    Node* cur = _node;
    Node* parent = _node->_parent;
    while (parent && parent->_right != cur)
    {
      cur = parent;
      parent = cur->_parent;
    }
    _node = parent;
  }
  return *this;
}
//后置--
Self operator--(int)
{
  Self tmp = *this;
  --(*this);
  return tmp;
}


至于为何要这两种不同的情况进行移动,上面的 正向移动 已经解释过了

以上就是 红黑树 中迭代器移动操作的相关实现

注意:在访问父亲节点前,需要先判断父亲是否为 nullptr,避免野指针

1.2.3、数据访问

数据访问 有两种方式:

  1. 直接解引用获取节点中的 _kv
  2. 获取节点中的 _kv 地址

具体实现如下:

//解引用
Ref operator*()
{
  return _node->_kv;
}
//成员访问
Ptr operator->()
{
  return &(operator*());  //复用
}


普通迭代器 创建对象时,传递的参数如下:

__RBTreeIterator<K, V, std::pair<K, V>&, std::pair<K, V>*>


此时的 RefPtr 就是普通的类型,允许发生 修改 行为

const 迭代器 创建对象时,传递的参数如下:

__RBTreeIterator<K, V, const std::pair<K, V>&, const std::pair<K, V>*>


RefPtrconst 对象,即不允许发生 修改 行为

这样一来,就能只通过一个 迭代器类,满足两个性质不同的 迭代器,这就是 泛型编程 思想的魅力

1.2.4、逻辑判断

在进行 迭代器逻辑判断 时,可以直接两个 红黑树 节点是否为同一个

//判断相等
bool operator==(const Self& it) const
{
  return _node == it._node;
}
bool operator!=(const Self& it) const
{
  return !((*this) == it);  //复用


注意:是迭代器和迭代器比较,所以参数是 Self 即迭代器对象

1.2.5、迭代器测试

有了这些模块后,我们的 红黑树 类中就可以引入 迭代器 的相关操作了

//新增迭代器
typedef __RBTreeIterator<K, V, std::pair<K, V>&, std::pair<K, V>*> iterator;
typedef __RBTreeIterator<K, V, const std::pair<K, V>&, const std::pair<K, V>*> const_iterator;
iterator begin()
{
  //起始位置是最左节点
  Node* cur = _root;
  while (cur && cur->_left != nullptr)
    cur = cur->_left;
  return iterator(cur);
}
iterator end()
{
  return nullptr;
}
const_iterator begin() const
{
  //起始位置是最左节点
  Node* cur = _root;
  while (cur && cur->_left != nullptr)
    cur = cur->_left;
  return const_iterator(cur);
}
const_iterator end() const
{
  return nullptr;
}


先来简单玩玩这个 迭代器

void RBTreeTest2()
{
  vector<pair<int, string>> vp{ make_pair(1,"a"),make_pair(2,"b"),make_pair(3,"c"),make_pair(4,"d"),make_pair(5,"e") };
  RBTree<int, string> rb;
  for (auto& e : vp)
    rb.Insert(e);
  const RBTree<int, string> crb(rb);
  cout << "普通对象: " << endl;
  RBTree<int, string>::iterator it = rb.begin();
  while (it != rb.end())
  {
    //两种访问方式都行,但推荐第二种,更方便
    cout << (*it).first << " | " << it->second << endl;
    ++it;
  }
  cout << "const 对象: " << endl;
  RBTree<int, string>::const_iterator cit = crb.begin();
  while (cit != crb.end())
  {
    cout << cit->first << " | " << (*cit).second << endl;
    cit++;  //后置++ 也可以用
  }
}


此时基于迭代器的范围 for 也可以正常使用

注意:const 迭代器是为 const 对象提供的,所以可以选择重载 begin()end(),也可以选择重新编写 cbegin()cend(),二者除了函数名外,其他都是一样的

1.3、反向迭代器的设计

红黑树 的反向迭代器比较难搞,因为 反向迭代器类 中为了追求极致对称,rbegin() 是最后一个节点的下一个节点,即 红黑树中最右节点的下一个节点

由于 三叉链 结构的特殊性,我们实现的 红黑树 中没有指向 最右节点 的节点,既然没有,那就创造一个,这正是 SGISTL红黑树 的实现方法,它的根节点 _root 的父亲不是空,而是搞了一个 header 节点,让根节点指向它,它的左右指针分别指向 最左节点最右节点

新增了这个节点后,之前的逻辑都要发生改变,比如 end() 不再是空,而是 header;涉及最左/最右节点的插入后,都要更新 header 的指向,这种方法在进行迭代器操作时比较友好,其他场景下就比较麻烦了,需要额外维护一个节点

如果按库中的 红黑树定义rbegin() 就是 header 这个节点,因为它指向 最右节点

为了避免破坏前面的操作,我们可以额外新增一个成员:header 指向最右节点,在调用 rbegin() 时对其进行更新并返回即可,属于一个比较 中庸 的解决方案,默认构造、拷贝构造、赋值、析构记得对这个节点进行处理

额外新增一个节点,不会影响其他节点,也不会影响前面的逻辑

private:
  Node* _root = nullptr;
  Node* _header = nullptr;  //指向最右节点
• 1
• 2
• 3


涉及构造、初始化等操作时,需要带上 _header

反向迭代器的设计如下:

#include "reverse_iterator.hpp"
typedef __reverse_iterator<iterator, std::pair<K, V>&, std::pair<K, V>*> reverse_iterator;  //反向迭代器
typedef __reverse_iterator<const_iterator, const std::pair<K, V>&, const std::pair<K, V>*> const_reverse_iterator;
//反向迭代器
reverse_iterator rbegin()
{
  //返回指向最右节点的节点
  Node* cur = _root;
  while (cur && cur->_right != nullptr)
    cur = cur->_right;
  _header->_left = cur;
  return reverse_iterator(_header);
}
reverse_iterator rend()
{
  //返回最后一个节点的上一个节点,即最左节点
  Node* cur = _root;
  while (cur && cur->_left != nullptr)
    cur = cur->_left;
  return reverse_iterator(cur);
}
const_reverse_iterator rbegin() const
{
  //返回指向最右节点的节点
  Node* cur = _root;
  while (cur && cur->_right != nullptr)
    cur = cur->_right;
  _header->_left = cur;
  return const_reverse_iterator(_header);
}
const_reverse_iterator rend() const
{
  //返回最后一个节点的上一个节点,即最左节点
  Node* cur = _root;
  while (cur && cur->_left != nullptr)
    cur = cur->_left;
  return const_reverse_iterator(cur);
}


为什么一定要搞一个 辅助节点指向最右节点?

  • 因为反向迭代器类比较奇怪 rbegin() 表示的是最后一个节点的下一个节点,所以为了与之适配,只能新增一个辅助节点

关于反向迭代器类的实现详见 《C++ STL学习之【反向迭代器】

其实库中解决方案是最优的,但这种方案会影响到前面的很多代码逻辑,于是我们选择了较为折中的方案

可以简单测试一下反向迭代器:

至此 红黑树 算是完善了,比较麻烦的是 迭代器 的实现,需要对 ++-- 进行分析,借助辅助节点 _header,最后也是成功利用 反向迭代器适配器 适配出了 红黑树的反向迭代器

注意:_header_left 链接 最右节点,因为反向迭代器中的 ++ 相当于 --,下一个节点是左子树的最右节点,就是整个红黑树中的最右节点


2、封装实现

下面可以正式步入本文的主题:用一棵红黑树封装实现 setmap

红黑树的封装实现会涉及部分代码改动

为了进行区分,红黑树的完善代码名为 RBTree - 副本.hpp 存放在 Gitee 仓库中

2.1、解决 k 与 k/v 的参数冲突

在同时封装 setmap 时,面临第一个问题:两者参数不匹配

  • set 只需要 key
  • map 则需要 keyvalue

这就意味着一棵 红黑树 无法满足不同需求,难道真无法满足吗?

答案当然是 可以的

参考库中的解决方案:管你是 k 还是 k/v,我都看作 value_type,获取 key 值时再另想其他方法解决

注:re_tree 的参数3是获取 key 的方式(后续介绍),参数4是比较方式,参数5是空间配置器

能否省略 参数1 key_type

  • 对于 set 来说,可以,因为冗余了
  • 但对于 map 来说,不行,因为 map 中的函数参数类型为 key_type,省略后就无法确定参数类型了,比如 FindErase 中都需要 key_type 这个类型

这一波是 setmap 做出了牺牲,迁就了 map

红黑树 改造第一步:接口调整

注:库中的 value_type 太长了,这里改为 T,既能表示 k,也能表示 k/v;原红黑树节点中的 _kv 改成了 _data

红黑树 从之前的 K V 变成了现在的 K T,这样一来,凡是之前涉及 K V 的地方都要改,比如:节点类 和 迭代器

//红黑树的节点类
template<class T>
struct RBTreeNode
{
  RBTreeNode(T data = T())
    :_left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _data(data)
    , _col(RED) //默认新节点为红色,有几率被调整
  {}
  //拷贝构造
  RBTreeNode(const T*& node)
    :_left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _data(node->_data)
    , _col(node->_col)  //默认新节点为红色,有几率被调整
  {
    //拷贝节点中的信息
  }
  RBTreeNode<T>* _left;
  RBTreeNode<T>* _right;
  RBTreeNode<T>* _parent;
  T _data;
  Color _col;
};
//迭代器类
template<class T, class Ref, class Ptr>
class __RBTreeIterator
{
  typedef RBTreeNode<T> Node; //节点
  typedef __RBTreeIterator<T, Ref, Ptr> Self; //迭代器
  //……
};
//红黑树类
template<class K, class T>
class RBTree
{
  typedef RBTreeNode<T> Node;
public:
  //……
  //新增迭代器
  typedef __RBTreeIterator<T, T&, T*> iterator;
  typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
  typedef __reverse_iterator<iterator, T&, T*> reverse_iterator;  //反向迭代器
  typedef __reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
  //……
};


除此之外仍有许多需要修改的地方,这里就不一一展示

红黑树 的接口经过这样一改之后,setmap 就可以传递各自的参数了

Set.hpp

#pragma once
#include <iostream>
#include "RBTree.hpp"
using Yohifo::RBTree;
namespace Yohifo
{
  template<class K>
  class set
  {
    typedef RBTree<K, K> Tree;
  private:
    Tree _t;  //这是一棵红黑树树
  };
}


Map.hpp

#pragma once
#include <iostream>
#include "RBTree.hpp"
using Yohifo::RBTree;
namespace Yohifo
{
  template<class K, class V>
  class map
  {
    typedef RBTree<K, std::pair<const K, V>> Tree;
  private:
    Tree _t;  //这也是一棵红黑树
  };
}


注意:mappair 的第一个参数 K 需要使用 const 修饰,因为这是不可被修改的

接下来就很简单了,直接使用 红黑树 中的接口即可(此处给 红黑树 新增了一个 Find 函数,代码如下)

bool Find(const K& key) const
{
  if (_root == nullptr)
    return false;
  Node* cur = _root;
  while (cur)
  {
    if (cur->_data.first < key)
      cur = cur->_right;
    else if (cur->_data.second > key)
      cur = cur->_left;
    else
      return true;
  }
  return false;
}


可以看到,Find() 的参数类型为 K

此时面临着一个尴尬的问题:Tkey 时,_data 不是 pair,自然没有 firstsecond,程序也就无法跑起来

Insert() 也是如此,凡是涉及获取 key 的地方都有这个问题,因为此时的 _data 是不确定的,对于这种不确定的类型,一般使用 仿函数 解决

2.2、解决不同类型的 key 获取问题

现在可以看看库中 rb_tree 的参数3了,它是一个 函数对象,可以传递 仿函数,主要是用来从不同的 T 中获取 key

set 中的 key 值就是 key,而 map 中的 key 值是 pair<K, V> 中的 first

所以 红黑树 的接口继续改进,新增 KeyOfT 这个模板参数

注:此时只需要在 红黑树类 中新增

//红黑树类
template<class K, class T, class KeyOfT>
class RBTree
{
  //……  


分别针对这两种不同的情况设计仿函数:

Set.hpp

template<class K>
class set
{
  //仿函数:获取 key 值
  struct SetKeyOfT
  {
    const K& operator()(const K& key) const
    {
      return key;
    }
  };
  typedef RBTree<K, K, SetKeyOfT> Tree;
private:
  Tree _t;  //这是一棵红黑树树
};


Map.hpp

template<class K, class V>
class map
{
  //仿函数:获取 key 值
  struct MapKeyOfT
  {
    const K& operator()(const std::pair<K, V>& kv) const
    {
      return kv.first;
    }
  };
  typedef RBTree<K, std::pair<K, V>, MapKeyOfT> Tree;
private:
  Tree _t;  //这也是一棵红黑树
};


这一波依然是 set 为了 map 做出了牺牲~

至于 rb_tree 中参数3,也是一个仿函数,主要是用来规定 pair 中的比较方式的

当我们得到不同的 key 值获取方式后,就可以更改 红黑树 中相应的代码了

比如:查找、插入

bool Find(const K& key) const
{
  KeyOfT kot; //创建一个对象,用来获取 key 值
  if (_root == nullptr)
    return false;
  Node* cur = _root;
  while (cur)
  {
    //operator()(data) 运算符重载,根据不同的对象,使用不同的获取方式
    if (kot(cur->_data) < key)
      cur = cur->_right;
    else if (kot(cur->_data) > key)
      cur = cur->_left;
    else
      return true;
  }
  return false;
}
bool Insert(const T& data)
{
  KeyOfT kot;
  if (_root == nullptr)
  {
    _root = new Node(data);
    _root->_col = BLACK;  //根节点一定是黑色
    return true;
  }
  //寻找合适位置
  Node* parent = nullptr;
  Node* cur = _root;
  while (cur)
  {
    if (kot(cur->_data) < kot(data))
    {
      parent = cur;
      cur = cur->_right;
    }
    else if (kot(cur->_data) > kot(data))
    {
      parent = cur;
      cur = cur->_left;
    }
    else
    {
      //插入失败
      return false;
    }
  }
  //插入节点
  cur = new Node(data);
  if (kot(parent->_data) < kot(data))
    parent->_right = cur;
  else
    parent->_left = cur;
  cur->_parent = parent;
  //判断是否需要 染色、旋转
  //……
}


现在代码可以跑起来了,先简单填充一下 setmap 中的基本操作

Set.hpp

public:
  bool Find(const K& key)
  {
    return _t.Find(key);
  }
  bool Insert(const K& key)
  {
    return _t.Insert(key);


Map.hpp

public:
  bool Find(const K& key)
  {
    return _t.Find(key);
  }
  bool Insert(const std::pair<K, V>& kv)
  {
    return _t.Insert(kv);
  }


测试自己封装的 setmap

void SetAndMapTest1()
{
  set<int> s;
  map<int, int> m;
  s.Insert(1);
  s.Insert(2);
  s.Insert(3);
  s.Insert(4);
  s.Insert(5);
  m.Insert(make_pair(1, 1));
  m.Insert(make_pair(2, 2));
  m.Insert(make_pair(3, 3));
  m.Insert(make_pair(4, 4));
  m.Insert(make_pair(5, 5));
  for (int i = 3; i < 8; i++)
  {
    cout << "set Find " << i << " -> " << s.Find(i) << endl;
    cout << "map Find " << i << " -> " << m.Find(i) << endl;
    cout << "=====================================" << endl;
  }
}


查找插入 没有问题(其实只要底层结构足够稳定,像这种表面封装是不太容易出问题的)

继续完善

  • set 新增迭代器、判空、求大小、计数
  • map 也是一样,新增 set 中新增的功能

注:关于默认成员函数,编译器生成的足够用了,因为此时的 _t 是一个自定义类型,涉及拷贝、赋值等问题时,会去调用 红黑树 中相应的函数,所以我们不需要实现

还是那句话:底层的数据结构足够强大,封装的时候就不需要操太多心

对于 setmap 都需要的函数,可以在 红黑树 中统一实现,两者分别调用即可

RBTree.hpp

bool Empty() const
{
  return _root == nullptr;
}
size_t Size() const
{
  //将底层容器遍历一遍即可
  size_t cnt = 0;
  for (auto& e : *this)
    cnt++;
  return cnt;
}
size_t Count(const K& key) const
{
  KeyOfT kot;
  //统计 key 的数量
  size_t cnt = 0;
  for (auto& e : *this)
  {
    //此时的 e 就是 key
    if (kot(e) == key)
      cnt++;
  }
  return cnt;
}


Set.hpp

注:typename 的作用是告诉编译器这是一个类型

//迭代器
typedef typename Tree::iterator iterator;
typedef typename Tree::const_iterator const_iterator;
typedef typename Tree::reverse_iterator reverse_iterator;
typedef typename Tree::const_reverse_iterator const_reverse_iterator;
iterator begin() { return _t.begin(); }
iterator end() { return _t.end(); }
const_iterator begin() const { return _t.begin(); }
const_iterator end() const { return _t.end(); }
reverse_iterator rbegin() { return _t.rbegin(); }
reverse_iterator rend() { return _t.rend(); }
const_reverse_iterator rbegin() const { return _t.rbegin(); }
const_reverse_iterator rend() const { return _t.rend(); }
bool Empty() const
{
  return _t.Empty();
}
size_t Size() const
{
  return _t.Size();
}
size_t Count(const K& key) const
{
  return _t.Count(key);
}


Map.hpp

//迭代器
typedef typename Tree::iterator iterator;
typedef typename Tree::const_iterator const_iterator;
typedef typename Tree::reverse_iterator reverse_iterator;
typedef typename Tree::const_reverse_iterator const_reverse_iterator;
iterator begin() { return _t.begin(); }
iterator end() { return _t.end(); }
const_iterator begin() const { return _t.begin(); }
const_iterator end() const { return _t.end(); }
reverse_iterator rbegin() { return _t.rbegin(); }
reverse_iterator rend() { return _t.rend(); }
const_reverse_iterator rbegin() const { return _t.rbegin(); }
const_reverse_iterator rend() const { return _t.rend(); }
bool Empty() const
{
  return _t.Empty();
}
size_t Size() const
{
  return _t.Size();
}
size_t Count(const K& key) const
{
  return _t.Count(key);
}


可以简单测试一波

注意:在 红黑树 中,凡是涉及 key 获取的地方,都要通过 KeyOfT 的方式进行获取,因为 _data 的类型不确定

2.3、解决 set 迭代器的非法操作

此时的代码仍然存在问题:set 中只有 keykey 是不能修改的,但此时 set 中的 key 可以被修改!

void SetAndMapTest3()
{
  vector<int> arr = { 8,6,3,2,1 };
  set<int> s;
  for (auto e : arr)
    s.Insert(e);
  cout << "修改前: ";
  for (auto& e : s)
  {
    cout << e << " ";
    e = 1;
  }
  cout << endl;
  cout << "修改后: ";
  for (auto& e : s)
    cout << e << " ";
  cout << endl;
}


此时居然能将 set 中的 key 进行修改!?这是非常不合理的

库中给出的解决方案:对于 set 来说,无论是否为 const 迭代器,都使用 红黑树中的 const 迭代器进行适配

也就是说,锁死了 set 中迭代器的修改权限,此时自然无法修改 key

  • 此时迭代器类中的 RefPtr 都是 const 版本

Set.hpp

//迭代器
typedef typename Tree::const_iterator iterator;
typedef typename Tree::const_iterator const_iterator;
typedef typename Tree::const_reverse_iterator reverse_iterator;
typedef typename Tree::const_reverse_iterator const_reverse_iterator;


修改完成,VS 启动,代码,运行

结果:出现了一个编译错误

注意:先要把修改相关的代码屏蔽,否则会导致这个错误无法出现

出现错误的原因

  • set 中,普通对象调用 begin()end() 时,返回的是 普通迭代器,但此时的 iteratorconst 迭代器,这就涉及一个类型转换问题了,其中的 RefPtr 类型不匹配!

解决方案:在 红黑树迭代器类 中新增一个特殊的构造函数

  • 当类模板实例化为 普通迭代器 时,就是一个普通的 拷贝构造 函数
  • 当类模板实例化为 const 迭代器 时,则是一个特殊的构造函数 -> 将普通的迭代器对象 -> 构造为 const 迭代器
typedef __RBTreeIterator<T, T&, T*> iterator; //普通迭代器
//特殊的构造函数
__RBTreeIterator(const iterator& it)
  :_node(it._node)  //构造 或 拷贝构造
{}


如何做到的?

  • 当创建 set(普通对象) 中的普通迭代器时,因为此时是普通对象,所以 红黑树 底层会返回一个 普通迭代器,但对于 set 来说,无论是否为 const 对象,它要返回的都是 const 迭代器,于是它会把 红黑树返回的普通迭代器 -> 借助特殊的构造函数 -> 构造为 const 迭代器
  • 如果 setconst 对象,那么 红黑树 返回的就是 const 迭代器,都不用进行类型转换了

这种写法对于 map 是否有影响?

  • 没有影响,对于 map 来说,普通对象对应的就是普通迭代器,不存在 普通迭代器 转为 const 迭代器 这种情况

新增这个特殊的构造函数后,能正常编译,将 e = 1 这条赋值语句取消注释,再编译,可以发现出现了预料中的报错信息:不能给常量对象赋值

注意:set 中的普通对象对应的也是 const 迭代器,但底层 红黑树 仍然是普通对象,返回的普通迭代器无法转换为 set 中的 const 迭代器,需要通过特殊构造函数解决;不能单纯的通过 const 修饰迭代器暴力解决问题,因为这样会出现 const const 的问题

2.4、调整函数返回值

setmap 中部分函数的返回值比较特殊,不是单纯的 bool

比如 Find() 返回的是 迭代器,查找成功返回所在位置的迭代器,失败返回最后一个位置的迭代器

Insert 插入时,成功返回 《新节点所在位置迭代器 与 true》 构成的 pair,失败则返回 《冗余节点所在位置的迭代器 与 false》 构成的 pair

红黑树 中的对应函数进行改造

RBTree.hpp

iterator Find(const K& key) const
{
  KeyOfT kot; //创建一个对象,用来获取 key 值
  if (_root == nullptr)
    return iterator(nullptr);
  Node* cur = _root;
  while (cur)
  {
    //operator()(data) 运算符重载,根据不同的对象,使用不同的获取方式
    if (kot(cur->_data) < key)
      cur = cur->_right;
    else if (kot(cur->_data) > key)
      cur = cur->_left;
    else
      return iterator(cur);
  }
  return iterator(nullptr);
}
std::pair<iterator, bool> Insert(const T& data)
{
  KeyOfT kot;
  if (_root == nullptr)
  {
    _root = new Node(data);
    _root->_col = BLACK;  //根节点一定是黑色
    return std::make_pair(iterator(_root), true);
  }
  //寻找合适位置
  Node* parent = nullptr;
  Node* cur = _root;
  while (cur)
  {
    if (kot(cur->_data) < kot(data))
    {
      parent = cur;
      cur = cur->_right;
    }
    else if (kot(cur->_data) > kot(data))
    {
      parent = cur;
      cur = cur->_left;
    }
    else
    {
      //插入失败
      return std::make_pair(iterator(cur), true);
    }
  }
  //插入节点
  //……
  //判断是否需要 染色、旋转
  //……
  return std::make_pair(iterator(new_node), true);
}


setmap 中做出相应调整即可

Set.hpp

iterator Find(const K& key) const
{
  return _t.Find(key);
}
std::pair<iterator, bool> Insert(const K& key)
{
  return _t.Insert(key);
}


Map.hpp

iterator Find(const K& key) const
{
  return _t.Find(key);
}
std::pair<iterator, bool> Insert(const std::pair<K, V>& kv)
{
  return _t.Insert(kv);


可以通过代码测试一下

清楚了 Insert 的返回值之后,就可以轻而易举的理解下图了

2.5、map 新增 operator[]

mapset 多一个 operator[] ,主要作用是用来 访问 value

并且 operator[] 还是一个多功能函数,兼顾 插入、修改、插入+修改、查找

具体实现如下:

Map.hpp

V& operator[](const K& key)
{
  //首先插入
  auto ret = Insert(std::make_pair(key, V()));
  //插入成功:获取新迭代器
  //插入失败:返回已存在节点迭代器
  auto it = ret.first;  //获取迭代器
  return it->second;  //返回 value
}

可以测试一下:

至此,用一颗 红黑树 完成 setmap 的封装就算完成了


3、性能测试

将自己封装的 set 与库中的 set 进行一波性能对比(Release 模式下)

#include <set>
void SetAndMapTest6()
{
  Yohifo::set<int> mySet;
  std::set<int> stdSet;
  srand((size_t)time(NULL));
  int mySetTime = 0;
  int stdSetTime = 0;
  clock_t begin, end;
  int sum = 0;
  int n = 5000000;
  for (int i = 0; i < n; i++)
  {
    int val = rand() % n + i;
    begin = end = 0;
    begin = clock();
    auto ret1 = mySet.Insert(val);
    end = clock();
    mySetTime += (end - begin);
    begin = end = 0;
    begin = clock();
    auto ret2 = stdSet.insert(val);
    end = clock();
    stdSetTime += (end - begin);
    if (ret1.second && ret2.second)
      sum++;  //成功插入的数据量
  }
  cout << "成功插入 " << sum << " 个数据" << endl;
  cout << "mySet 耗时: " << mySetTime << " ms" << endl;
  cout << "stdSet 耗时: " << stdSetTime << " ms" << endl;
}


成功插入 300w+ 数据,结果与库中的 set 性能差不多,证明我们这棵红黑树还是很强的


4、完整源码

关于本次完善的红黑树、封装实现 setmap 的相关代码在下面这个 Gitee 仓库中

封装set和map博客


🌆总结

以上就是本次关于 C++【一棵红黑树封装 set 和 map】的全部内容了,在本文中,我们首先将 红黑树 进行了完善,解决了一些深拷贝问题,新增了迭代器,同时还用反向迭代器适配器适配出了 反向迭代器,当红黑树完善后,我们用同一棵红黑树同时封装实现了 setmap,其中涉及大量 泛型编程思想,值得仔细推敲


相关文章推荐

C++ 进阶知识

C++【红黑树】

C++【AVL树】

C++【set 和 map 学习及使用】

C++【二叉搜索树】

C++【多态】

C++【继承】

STL 之 泛型思想

C++【模板进阶】

C++【模板初阶】
目录
相关文章
|
1月前
|
Java 开发者
在Java的集合世界里,Set以其独特的特性脱颖而出,它通过“哈希魔法”和“红黑树防御”两大绝技
【10月更文挑战第13天】在Java的集合世界里,Set以其独特的特性脱颖而出。它通过“哈希魔法”和“红黑树防御”两大绝技,有效抵御重复元素的侵扰,确保集合的纯洁性和有序性。无论是“人海战术”还是“偷梁换柱”,Set都能从容应对,成为开发者手中不可或缺的利器。
31 6
|
1月前
|
存储 JavaScript 前端开发
Set、Map、WeakSet 和 WeakMap 的区别
在 JavaScript 中,Set 和 Map 用于存储唯一值和键值对,支持多种操作方法,如添加、删除和检查元素。WeakSet 和 WeakMap 则存储弱引用的对象,有助于防止内存泄漏,适合特定场景使用。
|
2月前
|
存储 Java API
【数据结构】map&set详解
本文详细介绍了Java集合框架中的Set系列和Map系列集合。Set系列包括HashSet(哈希表实现,无序且元素唯一)、LinkedHashSet(保持插入顺序的HashSet)、TreeSet(红黑树实现,自动排序)。Map系列为双列集合,键值一一对应,键不可重复,值可重复。文章还介绍了HashMap、LinkedHashMap、TreeMap的具体实现与应用场景,并提供了面试题示例,如随机链表复制、宝石与石头、前K个高频单词等问题的解决方案。
37 6
【数据结构】map&set详解
|
1月前
|
存储 缓存 Java
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
35 1
|
2月前
|
算法
你对Collection中Set、List、Map理解?
你对Collection中Set、List、Map理解?
36 5
|
2月前
|
存储 JavaScript 前端开发
js的map和set |21
js的map和set |21
|
2月前
|
存储 前端开发 API
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
该文章详细介绍了ES6中Set和Map数据结构的特性和使用方法,并探讨了它们在前端开发中的具体应用,包括如何利用这些数据结构来解决常见的编程问题。
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
|
2月前
|
数据安全/隐私保护 C语言 C++
C++(七)封装
本文档详细介绍了C++封装的概念及其应用。封装通过权限控制对外提供接口并隐藏内部数据,增强代码的安全性和可维护性。文档首先解释了`class`中的权限修饰符(`public`、`private`、`protected`)的作用,并通过示例展示了如何使用封装实现栈结构。接着介绍了构造器和析构器的使用方法,包括初始化列表的引入以及它们在内存管理和对象生命周期中的重要性。最后,通过分文件编程的方式展示了如何将类定义和实现分离,提高代码的模块化和复用性。
|
3月前
|
存储 Java 索引
|
3月前
|
存储 安全 Java
java集合框架复习----(4)Map、List、set
这篇文章是Java集合框架的复习总结,重点介绍了Map集合的特点和HashMap的使用,以及Collections工具类的使用示例,同时回顾了List、Set和Map集合的概念和特点,以及Collection工具类的作用。
java集合框架复习----(4)Map、List、set