用红黑树封装实现map和set

简介: 用红黑树封装实现map和set

map和set的实现原理

为了方便实现我们的map和set,我们肯定是要养成看源码的习惯的,看了源码之后你才会感受到大佬的强大!

在源码里面,对于map和set的实现,底层是用同一棵红黑树封装出来的,并不是用了两棵红黑树

那么大家肯定会有疑问了,一棵红黑树这么能两用呢,况且map和set的底层存储的节点类型不一样啊,map是存储的键值对,set只是存储key

这时设计map和set的大佬就想到了一个极佳的办法,在红黑树底层中用了一个模板参数Value来代表红黑树结点存储对象的类型,这个类型可能是pair键值对,也有可能是key类型。

红黑树其实并不知道自己的节点是什么类型,只能用模板来接受存储对象的类型

enum Color {RED,BLACK};
template<class T>//节点存储对象类型,可能是键值对也有可能是key
class RBTreeNode
{
  T _data;
  RBTreeNode<T>* _left;
  RBTreeNode<T>* _right;
  RBTreeNode<T>* _parent;
  Color _col;
  RBTreeNode(const T& data)
    :_data(data)
    , _left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _col(RED)
  {}
};

这样,我们就可以实现一树多用了!

我们就可以根据value的类型来去确定是map还是set了,但是大家可能会有一点小疑问,既然有了value就可以实现红黑树了,那我们还要关键码key有什么用呢?

这里的关键码key是必不可少的!

因为在红黑树搜索的时候,依靠的还是关键码进行搜索,通过所传关键码和红黑树结点中的关键码的大小关系,确定到红黑树中要搜索的某个结点。

红黑树的find函数的参数类型必须是key!

在实现map和set之前,我们先想一想,上一篇博文的红黑树有哪些地方需要进行改进的?

细心的朋友就会发现,上篇博文的代码,插入时比较的就只是在整形情况下的key的比较,但是map和set就只是存储整形吗?显然不是,所以我们就要想办法提取其他类型下的key关键码来进行比较,并且map的节点的value类型是键值对的话,要提取的first,set只需要直接比较key,该如何结局这个问题呢?

我们这个时候就可以使用仿函数了!

我们可以在map和set中各自增加一个仿函数类,在模板传参时,再增加一个仿函数类来提取节点的关键码的参数,我们将他命名为KeyOfT

set

template<class K>
class set
{
  struct SetKeyOfT
  {
    const K& operator()(const K& key)
    {
      return key;
    }
  };
public:
private:
  RBTree<K, K, SetKeyOfT> _t;
};

map

template<class K, class V>
class map
{
  struct MapKeyOfT
  {
    const K& operator()(const pair<const K, V>& kv)
    {
      return kv.first;
    }
  };
public:
private:
  RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};

rbtree

template<class K,class T,class KeyOfT>
class RBTree
{
  typedef RBTreeNode<T> Node;
public:
private:
  Node* _root = nullptr;
};

这样我们再插入的时候就可以直接提取关键码进行插入了

map和set的迭代器

关于map和set的迭代器的实现,设计库里面的源码的大佬很巧妙地运用了const

大家都知道,set是不可以修改的,map可以修改,但是修改的也只是pair键值对的value,pair里面的key可是不能修改的

对红黑树类型中的迭代器类型进行typedef时,可以看到我们在typedef后面加了typename,typename的作用就是告诉编译器后面的东西是一个类型,你先不要编译他,等到模板实例化为真正类型后,你再去取他里面的内嵌类型iterator。因为我们知道编译器是不编译模板的,编译的是模板实例化之后的代码,只有模板实例化之后,它里面的内嵌类型我们才可以取到,所以如果你不加typename,有可能取的不是类型,因为静态变量或函数都是可以通过类+域访问符进行访问的,所以如果你要取模板里面的类型,那就必须在模板前面加typename,告诉编译器你取的是类型。

rbtree迭代器实现

struct __RBTreeIterator
{
  typedef RBTreeNode<T> Node;
  typedef __RBTreeIterator<T> Self;
  Node* _node;
  __RBTreeIterator(Node* node)
    :_node(node)
  {}
  T& operator*()
  {
    return _node->_data;
  }
  T* operator->()
  {
    return &_node->_data;
  }
  Self& operator++()
  {
    if (_node->_right)
    {
      Node* min = _node->_right;
      while (min->_left)
      {
        min = min->_left;
      }
      _node = min;
    }
    else
    {
      Node* cur = _node;
      Node* parent = cur->_parent;
      while (parent && cur == parent->_right)
      {
        cur = cur->_parent;
        parent = parent->_parent;
      }
      _node = parent;
    }
    return *this;
  }
  Self& operator--()
  {
    //右子树 根 左子树
    if (_node->_left)
    {
      //找左子树的最右结点,其实就是次大的结点
      _node = _node->_left;
      while (_node->_right)
      {
        _node = _node->_right;
      }
    }
    else
    {
      Node* cur = _node;
      Node* parent = _node->_parent;
      while (parent && cur == parent->_left)
      {
        cur = parent;
        parent = parent->_parent;
      }
      //如果cur是parent的右,则直接让_node指向到parent即可
      _node = parent;
    }
    return *this;//返回迭代器对象
  }
  bool operator!=(const Self& it)const//const修饰表示在该成员函数内部,不能修改对象的任何成员变量
  {
    return this->_node != it._node;
  }
  bool operator==(const Self& it)const//const对象和非const对象都可以调用
  {
    return this->_node == it._node;
  }
};

那么map和set的实现就很简单了:

template <class K>  
  class set
  {
  public:
    //set表层的普通迭代器底层用的依旧是const_iterator,就是不允许对set的迭代器指向内容进行修改
    typedef typename RBTree<K, K, SetKeyOfT<K>>::const_iterator iterator;
    typedef typename RBTree<K, K, SetKeyOfT<K>>::const_iterator const_iterator;
    iterator begin()  const   //为了防止权限的放大,我们直接使用const,如果对象是const,调用const的begin,不是const也调用const的begin和end
    {
      return _t.begin();
    }
    iterator end()  const
    {
      return _t.end();
    }

map的迭代器是需要实现两个版本的,因为map容器中的数据是可以被修改的,如果你想修改map中的数据那就用iterator,如果你想修改map中的数据那就用const_iterator。
如果是const_iterator,那解引用或者→返回的就是键值对的常引用或const修饰的指向键值对的结构体指针,那么此时键值对的key和value都是不可以修改的。
如果是iterator,解引用或者→返回的就是键值对的普通引用或无const修饰的指向键值对的结构体指针,但此时键值对的key依旧不可以被修改,只能对键值对中的value进行修改所以即使你用的是iterator,他所指向的键值对的key依旧是不能修改的,我们只能修改他的value。

template <class K, class V>
class map
{
public:
  typedef typename RBTree<K, pair<const K, V>, MapKeyOfT<K, V>>::const_iterator const_iterator;
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT<K, V>>::const_iterator const_iterator;
  //map的迭代器需要提供两个版本
  iterator begin()
  {
    return _t.begin();
  }
  iterator end()
  {
    return _t.end();
  }
  const_iterator begin()const
  {
    return _t.begin();
  }
  const_iterator end()const
  {
    return _t.end();
  }
private:
  RBTree<K, pair<const K, V>, MapKeyOfT<K,V>> _t;
};

红黑树插入的改造

我们使得插入后其返回值为键值对,如果出现插入key值相等的情况,则将bool改为false即可。最后调色后返回键值对时,我们不能直接返回cur构造的迭代器,因为如果发生调色,则cur位置会更改,所以在插入结点后,用一个curit的结点指针记录一下插入结点的位置,最后返回由curit结点指针构造出来的迭代器

pair<iterator, bool> Insert(const T& data)
//红黑树必须通过结点里面存储的key来进行比较才可以插入结点,所以出现了kot仿函数对象
{
  KeyOfT kot;
  Node* parent = nullptr;
  Node* cur = _root;
  if (_root == nullptr)
  {
    _root = new Node(data);
    _root->_col = BLACK;
    return make_pair(iterator(_root), true);
  }
  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 make_pair(iterator(cur), false);
    }
  }
  cur = new Node(data);
  Node* curit = cur;//保存一下cur的位置否则下面旋转调色过程中,cur有可能被改了
  if (kot(parent->_data) > kot(data))
  {
    parent->_left = cur;
    cur->_parent = parent;
  }
  else
  {
    parent->_right = cur;
    cur->_parent = parent;
  }
  while (parent && parent->_col == RED)
  {
    Node* grandparent = parent->_parent;
    if (parent == grandparent->_left)
    {
      Node* uncle = grandparent->_right;
      if (uncle && uncle->_col == RED)
      {
        parent->_col = uncle->_col = BLACK;
        grandparent->_col = RED;
        cur = grandparent;
        parent = cur->_parent;
      }
      else if (cur == parent->_left)
      {
        RotateR(grandparent);
        grandparent->_col = RED;
        parent->_col = BLACK;
        break;
      }
      else if (cur == parent->_right)
      {
        RotateL(parent);
        RotateR(grandparent);
        cur->_col = BLACK;
        grandparent->_col = RED;
        break;
      }
      else
      {
        assert(false);
      }
    }
    else
    {
      Node* uncle = grandparent->_left;
      if (uncle && uncle->_col == RED)
      {
        grandparent->_col = RED;
        uncle->_col = parent->_col = BLACK;
        cur = grandparent;
        parent = cur->_parent;
      }
      else if (cur == parent->_right)
      {
        RotateL(grandparent);
        parent->_col = BLACK;
        grandparent->_col = RED;
        break;
      }
      else if (cur == parent->_left)
      {
        RotateR(parent);
        RotateL(grandparent);
        cur->_col = BLACK;
        grandparent->_col = RED;
        break;
      }
      else
      {
        assert(false);
      }
    }
  }
  _root->_col = BLACK;
  
  return make_pair(iterator(curit), true);
}

map的[]重载

[]直接返回插入节点的值,如果[]内的key值不在红黑树中,就直接插入,若在其中也有进行修改,最后返回iterator的first的second

template <class K, class V>
class map
{
public:
  pair<iterator, bool> insert(const pair<const K, V>& kv)
  {
    return _t.Insert(kv);
  }
  
  V& operator[](const K& key)
  {
    pair<iterator, bool> ret = insert(make_pair(key, V()));
    return (ret.first)->second;
  }
private:
  RBTree<K, pair<const K, V>, MapKeyOfT<K,V>> _t;
};

由于博主的能力有限,部分讲解不到位,等我巩固知识后再做修正!希望大家指正!本篇博文的分享到这里就结束了,感谢大家的支持!

相关文章
|
2月前
|
存储 JavaScript Java
(Python基础)新时代语言!一起学习Python吧!(四):dict字典和set类型;切片类型、列表生成式;map和reduce迭代器;filter过滤函数、sorted排序函数;lambda函数
dict字典 Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。 我们可以通过声明JS对象一样的方式声明dict
236 1
|
5月前
|
存储 缓存 JavaScript
Set和Map有什么区别?
Set和Map有什么区别?
467 1
|
2月前
|
存储 算法 容器
set_map的实现+set/map加持秒杀高频算法题锻炼算法思维
`set`基于红黑树实现,支持有序存储、自动去重,增删查效率为O(logN)。通过仿函数可自定义排序规则,配合空间配置器灵活管理内存。不支持修改元素值,迭代器失效需注意。`multiset`允许重复元素。常用于去重、排序及查找场景。
|
6月前
|
存储 JavaScript 前端开发
for...of循环在遍历Set和Map时的注意事项有哪些?
for...of循环在遍历Set和Map时的注意事项有哪些?
344 121
|
9月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
277 2
|
6月前
|
存储 C++ 容器
unordered_set、unordered_multiset、unordered_map、unordered_multimap的介绍及使用
unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。在unordered_set中,元素的值同时也是唯一地标识它的key。在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。它的迭代器至少是前向迭代器。前向迭代器的特性。
304 0
|
6月前
|
编译器 C++ 容器
用一棵红黑树同时封装出map和set
再完成上面的代码后,我们的底层代码已经完成了,这时候已经是一个底层STL的红黑树了,已经已符合库里面的要求了,这时候我们是需要给他穿上对应的“衣服”,比如穿上set的“衣服”,那么这个穿上set的“衣服”,那么他就符合库里面set的要求了,同样map一样,这时候我们就需要实现set与map了。因此,上层容器map需要向底层红黑树提供一个仿函数,用于获取T当中的键值Key,这样一来,当底层红黑树当中需要比较两个结点的键值时,就可以通过这个仿函数来获取T当中的键值了。我们就可以使用仿函数了。
91 0
|
6月前
|
存储 编译器 容器
set、map、multiset、multimap的介绍及使用以及区别,注意事项
set是按照一定次序存储元素的容器,使用set的迭代器遍历set中的元素,可以得到有序序列。set当中存储元素的value都是唯一的,不可以重复,因此可以使用set进行去重。set默认是升序的,但是其内部默认不是按照大于比较,而是按照小于比较。set中的元素不能被修改,因为set在底层是用二叉搜索树来实现的,若是对二叉搜索树当中某个结点的值进行了修改,那么这棵树将不再是二叉搜索树。
260 0
|
10月前
|
编译器 容器
哈希表模拟封装unordered_map和unordered_set
哈希表模拟封装unordered_map和unordered_set