【C++】map、set模拟实现

简介: 所谓低层被高层封装就是低层类要作为高层类的成员变量。比如红黑树和迭代器,它们的成员变量都是一个树的节点;而map和set类的成员变量是同一颗红黑树,它们的接口都是通过直接或间接调用红黑树的接口实现

一. 整体框架梳理

91380866a1b44ea28c767a7ae3b243c2.png

所谓低层被高层封装就是低层类要作为高层类的成员变量。比如红黑树和迭代器,它们的成员变量都是一个树的节点;而map和set类的成员变量是同一颗红黑树,它们的接口都是通过直接或间接调用红黑树的接口实现的。


二. 节点类


节点类是一个类模板,模板参数ValueType就是节点存储的数据的类型。成员变量包括指向父亲、左右孩子的指针,节点的颜色和节点存储的数据。成员函数就只有一个构造函数,传入一个ValueType的数据来构造一个节点。

// 枚举定义颜色
enum COLOR
{
  RED,
  BLACK
};
// 节点的定义
template<class ValueType>
struct RBTreeNode
{
  // 构造函数,颜色默认给为红色
  RBTreeNode(const ValueType& data)
    :_color(RED)
    ,_data(data)
    ,_parent(nullptr)
    ,_left(nullptr)
    ,_right(nullptr)
  {}
  COLOR _color;
  ValueType _data;
  RBTreeNode<ValueType>* _parent;                                                                                     
  RBTreeNode<ValueType>* _left;
  RBTreeNode<ValueType>* _right;
};

三. 迭代器


1. 迭代器的基本框架


底层封装一个树节点的指针_node,所以构造函数是传入一个树节点的指针让_node指向它。

template<class ValueType, class Ref, class Ptr>
class RBTreeItetator
{
    typedef RBTreeNode<ValueType> Node;
    typedef RBTreeItetator<ValueType, Ref, Ptr> self;
  public:
    RBTreeItetator(Node* node)
      :_node(node)
    {}
  private:
    Node* _node;
};

为了实现const迭代器,模板参数除了节点的数据类型外还引入了数据类型的引用(Reference)和数据类型的指针(Pointer)。在迭代器重载解引用和箭头运算符时,不论外面传入的模板参数是什么,都返回Ref和Ptr,可以看到这两个模板参数的引入使得代码更加灵活,这是模板优点的体现,也是泛型编程思想的一个体现。

6a0cdffada604c9e975c73b572f3dfa7.png


2. operator++ 和 operator- -


先看自增,我们想要这个迭代器指向下一个节点,根据中序遍历的规则可以得到下面结论:

如果右不为空,中序的下一个就是右子树的最左节点。

19510ff4cef54495a8aaed48079a24ea.png


如果右为空,表示当前节点所在的子树已经遍历完成,下一个节点为沿着路径往上孩子是它的左的那个祖先。

4743dff3ec15467a9c8d7ff6fd6950c5.png


自减操作的话,就是找前一个节点,和自增的思路相反,不再赘述。它们的实现如下:

// 前置自增操作
self& operator++()    
{    
  if(_node->_right)    
  {    
    Node* subRL=_node->_right;    
    while(subRL->_left)                                                                                                     
    {    
      subRL=subRL->_left;    
    }    
    _node=subRL;    
  }    
  else     
  {    
    Node* cur=_node;    
    Node* parent=cur->_parent;    
    while(parent && cur == parent->_right)    
    {    
      cur=parent;    
      parent=parent->_parent;    
    }    
    _node=parent;    
  }    
  return *this;    
}   
// 后置自减操作
self& operator--()
{
  if(_node->_left)
  {
    Node* subLR=_node->_left;
    while(subLR->_right)
    {
      subLR=subLR->_right;
    }
    _node=subLR;
  }
  else 
  {
    Node* cur=_node;
    Node* parent=cur->_parent;
    while(parent && cur==parent->_left)
    {
      cur=parent;
      parent=parent->_parent;
    }                                                                                                                       
    _node=parent;
  }
  return *this;
}


3. operator* 和 operator->


解引用就是返回节点中存储的数据的引用,箭头返回节点中存储数据的指针。

Ref operator*()
{
  return _node->_data;
}
Ptr operator->()
{
  return &_node->_data;
}

编译器对箭头操作符的特殊处理

所谓迭代器就是模拟像指针一样的东西,我们希望通过解引用迭代器来直接获取所存储的数据对象本身,我们上面的实现确实做到了;我们也希望通过箭头操作符操作迭代器直接访问存储数据内的成员,在使用箭头操作符时会遇到一些问题。

d8a941bc5879455aba66375a8921708d.png


4. operator== 和 operator!=


比较两个迭代器是否相等,就是比较底层的成员变量即节点对象是否相等。

bool operator==(const self& it)
{
  return _node == it._node;
}
bool operator!=(const self& it)
{
  return _node != it._node;
}


5. 迭代器完整代码

// 迭代器    
template<class ValueType, class Ref, class Ptr>    
class RBTreeItetator    
{    
    typedef RBTreeNode<ValueType> Node;    
    typedef RBTreeItetator<ValueType, Ref, Ptr> self;    
  public:                                                                                                                       
    RBTreeItetator(Node* node)    
      :_node(node)    
    {}    
    self& operator++()    
    {    
      if(_node->_right)    
      {    
        Node* subRL=_node->_right;    
        while(subRL->_left)    
        {    
          subRL=subRL->_left;    
        }    
        _node=subRL;    
      }    
      else     
      {    
        Node* cur=_node;
        Node* parent=cur->_parent;
        while(parent && cur == parent->_right)
        {
          cur=parent;
          parent=parent->_parent;
        }
        _node=parent;
      }
      return *this;
    }
    self& operator--()
    {
      if(_node->_left)
      {
        Node* subLR=_node->_left;
        while(subLR->_right)
        {                                                                                                                       
          subLR=subLR->_right;
        }
        _node=subLR;
      }
      else 
    {
        Node* cur=_node;
        Node* parent=cur->_parent;
        while(parent && cur==parent->_left)
        {
          cur=parent;
          parent=parent->_parent;
        }
        _node=parent;
      }
      return *this;
    }
    Ref operator*()
    {
      return _node->_data;
    }
    Ptr operator->()                                                                                                            
    {
      return &_node->_data;
    }
    bool operator==(const self& it)
    {
      return _node == it._node;
    }
    bool operator!=(const self& it)
    {
      return _node != it._node;
    }
  private:
    Node* _node;                                                                                                                
};

   

四. 红黑树


1. 红黑树基本框架


成员变量有两个:记录该树节点个数的_size和该树的根_root。

// 树的定义                                                                                                                     
template<class Key, class ValueType, class KeyOfValue>    
class RBTree     
{    
    typedef RBTreeNode<ValueType> Node;    
  public:    
    typedef RBTreeItetator<ValueType, ValueType&, ValueType*> Iterator;    
    typedef RBTreeItetator<ValueType, const ValueType&, const ValueType*> const_Iterator;    
    RBTree()    
      :_size(0)    
      ,_root(nullptr)    
    {} 
  private:
    size_t _size;
    Node* _root;
}; 

关于typedef的几点说明

我们对节点类重定义为Node,后面会经常用到为了方便写才重定义的,权限为private因为我们只在类内部使用。另外我们还对迭代器进行了重定义,包括const迭代器和非const迭代器,为了可读性和写起来便捷才重定义,权限为public因为有从外部读取的需要:


90138d9a7dca440087001103c17de3d7.png

如何用一棵红黑树同时实现map和set?

ec54ad7adadb40028c8eec449f8f6956.png


通过对模板参数的巧妙设计可以实现。第一个模板参数Key是节点排序和比较的唯一标识,如果是set的话key就是它自己本身存储的数据,如果是map的话key就是键值对的第一个值。第二个模板参数是map和set存储的数据,map的话是pair键值对,set的话是key。

cca9fd484b8246aba2f30ffeda953371.png


关于Insert中插入数据的比较方式


105bf2cccb20428995eca6ea2bf1883d.png

此时红黑树的第三个参数就起作用了,它是一个仿函数,map、set各自实现一个并传递给一同颗红黑树。KeyOfValue,顾名思义就是获取ValueType中的Key,即存储数据作为比较的唯一表示。虽然map和set的对KeyOfValue实现不同,但可以完美地匹配它们自己的ValuType,当插入时统一调用这个仿函数来获取Key进行比较,这又是一种泛型编程的体现。

2db20cdee51a4a7f9c0b7442ff290337.png

第一个模板参数的作用?

第二个模板参数和第三个模板参数我们都在Insert()中实用到了,传入的第一个模板参数Key有什么用呢?在Find()中就会用到第一个模板参数,我们调用Find()时传入的实参类型就是Key。

0ae54febf543411caa17ad7f2ea7c9c2.png


2. begin()和end()


红黑树的成员变量是整棵树的根节点,begin()要返回第一个节点的迭代器,根据中序遍历的规则往前找就是去找左子树的最左节点;end()的话就返回一个空的迭代器。最后普通对象返回普通迭代器,const对象返回const迭代器,至于对象是普通的还是const的由在我们在创建map或set对象时它们的属性来决定,因为map和set底层封装红黑树,它们的对象如果是const的话,封装的红黑树也是const的,对应返回const迭代器。

Iterator Begin()    
{    
  Node* subL=_root;    
  while(subL && subL->_left)    
  {    
    subL=subL->_left;    
  }    
  return Iterator(subL);    
}    
const_Iterator Begin() const    
{    
  Node* subL=_root;    
  while(subL && subL->_left)    
  {    
    subL=subL->_left;    
  }    
  return const_Iterator(subL);   
  }
Iterator End()
{
  return Iterator(nullptr);
}
const_Iterator End() const
{
  return const_Iterator(nullptr);
}

3. empty()和size()


红黑树除了根节点外还有一个记录当前节点数的成员变量_size。

empty()的话就是判断_size是否等于0。

size()的话直接返回_size的值就行。

size_t Size() const 
{
  return _size;
}
bool Empty() const 
{
  return _size == 0;
}

4. Insert 和 Find


这些都是红黑树的基本操作,没有什么太多特别的和与其他类相关联的东西,相关代码放到下面的完整代码里。一定要说的话就是他们的返回值:

  • Insert返回一个pair键值对,它的first为节点的迭代器,second为布尔值。如果插入成功first为新插入节点的迭代器,second为true;如果插入失败(要插入的Key已经存在),此时pair的first为那个已经存在的节点的迭代器,second为false。
  • Find返回一个迭代器,如果找到了返回该节点的迭代器,找不到返回空迭代器,即end()


5. 红黑树完整代码

// 红黑树的定义
template<class Key, class ValueType, class KeyOfValue>
class RBTree 
{
    typedef RBTreeNode<ValueType> Node;
  public:
    typedef RBTreeItetator<ValueType, ValueType&, ValueType*> Iterator;
    typedef RBTreeItetator<ValueType, const ValueType&, const ValueType*> const_Iterator;
    RBTree()
      :_size(0)
      ,_root(nullptr)
    {}
    Iterator Begin()
    {
      Node* subL=_root;
      while(subL && subL->_left)
      {
        subL=subL->_left;
      }
      return Iterator(subL);
    }
    const_Iterator Begin() const
    {
      Node* subL=_root;
      while(subL && subL->_left)
      {
        subL=subL->_left;
      }
      return const_Iterator(subL);
    }
    Iterator End()
    {
      return Iterator(nullptr);
    }
    const_Iterator End() const
    {
      return const_Iterator(nullptr);
    }
    size_t Size() const 
    {
      return _size;
    }
    bool Empty() const 
    {
      return _size == 0;
    }
    // 插入一个节点
    pair<Iterator, bool> Insert(const ValueType& data)
    {
      // 第一步:按搜索树规则插入一个节点
      // 1、空树的话插入节点作为根节点
      // 2、不空的话,按搜索树规则找到插入的位置,然后插入
      if(!_root)
      {
        _root=new Node(data);
        _root->_color=BLACK;
        ++_size;
        return make_pair(Iterator(_root), true);
      }
      // 寻找插入的位置
      KeyOfValue kov;
      Node* parent=nullptr;
      Node* cur=_root;
      while(cur)
      {
        parent=cur;
        if(kov(cur->_data) == kov(data))
        {
          return make_pair(Iterator(cur), false);
        }
        else if(kov(cur->_data) < kov(data))
        {
          cur=cur->_right;
        }
        else if(kov(cur->_data) > kov(data))
        {
          cur=cur->_left;
        }
      }
      // 找到后插入
      Node* newNode=new Node(data);
      newNode->_parent=parent;
      if(kov(parent->_data) > kov(data))
      {
        parent->_left=newNode;
      }
      else if(kov(parent->_data) < kov(data)) 
      {
        parent->_right=newNode;
      }
      // 第二步:调整节点的颜色
      // 1、如果父亲颜色为黑就不用调整
      // 2、如果父亲节点颜色为红,调整方式由叔叔决定
      //    a、叔叔存在且为红 --> 把叔叔和父亲的颜色调整为黑,爷爷颜色调整为红,然后继续往上调整
      //    b、叔叔不存在或存在且为黑 --> 看爷爷、父亲、和cur的相对位置来决定用什么旋转,旋转后调整父亲和爷爷的颜色
      cur=newNode;
      while(parent &&  parent->_color == RED)
      {
        // 既然进来了这个循环内部,说明父亲一定是红色,那么一定有爷爷且爷爷颜色为黑
        Node* grandfather = parent->_parent;
        // 确定叔叔的位置
        if(grandfather->_left == parent)
        {
          Node* uncle=grandfather->_right;
          if(uncle && uncle->_color == RED)// 叔叔存在且为红
          {
            parent->_color=uncle->_color=BLACK;
            grandfather->_color=RED;
            cur=grandfather;
            parent=cur->_parent;
          }
          else // 叔叔不存在或者存在且为黑
          {
            if(cur == parent->_right)
            {
              RotateL(parent);
              std::swap(parent,cur);
            }
            RotateR(grandfather);
            parent->_color=BLACK;
            grandfather->_color=RED;
            break;
          }
        }
        else if(grandfather->_right == parent)
        {
          Node* uncle=grandfather->_left;
          if(uncle && uncle->_color==RED)// 叔叔存在且为红
          {
            uncle->_color=parent->_color=BLACK;
            grandfather->_color=RED;
            cur=grandfather;
            parent=cur->_parent;
          }
          else // 叔叔不存在或存在且为黑
          {
            if(cur == parent->_left)
            {
              RotateR(parent);
              std::swap(parent,cur);
            }
            RotateL(grandfather);
            parent->_color=BLACK;
            grandfather->_color=RED;
            break;
          }
        }
      }
      _root->_color=BLACK;
      ++_size;
      return make_pair(Iterator(newNode), true); 
    }
    KeyOfValue kov;
   Iterator Find(const Key& k)
   {
     Node* cur=_root;
     while(cur)
     {
       if(kov(cur->_data) > k)
       {
         cur=cur->_left;
       }
       else if(kov(cur->_data) < k)
       {
         cur=cur->_right;
       }
       else 
       {
         return Iterator(cur);
       }
     }
     return Iterator(nullptr);
   }
   const_Iterator Find(const Key& k) const
   {
     Node* cur=_root;
     while(cur)
     {
       if(kov(cur->_data) > k)
       {
         cur=cur->_left;
       }
       else if(kov(cur->_data) < k)
       {
         cur=cur->_right;
       }
       else 
       {
         return const_Iterator(cur);
       }
     }
     return const_Iterator(nullptr);
    }
 private:
    // 左单旋
    void RotateL(Node* parent)
    {
      Node* subR=parent->_right;
      Node* subRL=subR->_left;
      parent->_right=subRL;
      if(subRL != nullptr)
      {
        subRL->_parent=parent;
      }
      subR->_left=parent;
      Node* pparent=parent->_parent;
      parent->_parent=subR;
      if(pparent == nullptr)
      {
        _root=subR;
        subR->_parent=nullptr;
      }
      else 
      {
        subR->_parent=pparent;
        if(pparent->_left == parent)
        {
          pparent->_left=subR;
        }
        else if(pparent->_right == parent)
        {
          pparent->_right=subR;
        }
      }
    }
    // 右单旋
    void RotateR(Node* parent)
    {
      Node* subL=parent->_left;
      Node* subLR=subL->_right;
      parent->_left=subLR;
      if(subLR != nullptr)
      {
        subLR->_parent=parent;
      }
      subL->_right=parent;
      Node* pparent=parent->_parent;
      parent->_parent=subL;
      if(pparent == nullptr)
      {
        _root=subL;
        subL->_parent=nullptr;
      }
      else 
      {
        subL->_parent=pparent;
        if(pparent->_left == parent)
        {
          pparent->_left=subL;
        }
        else if(pparent->_right == parent) 
        {
          pparent->_right=subL;
        }
      }
    }
    size_t _size;
    Node* _root;
};


五. map和set


1. map、set的基本框架


它们两个的成员变量都是一颗红黑树,并且都自己实现仿函数KeyOfValue。不同点在于模板参数不同,map因为存储键值对所以有两个模板参数;set只有一个模板参数Key。

// map的基本框架
template<class K, class V>    
class MyMap    
{    
    typedef pair<K, V> ValueType;    
    struct KeyOfValue     
    {    
      const K& operator()(const ValueType& v)    
      {    
        return v.first;    
      }    
    }    
  private:    
    RBTree<K, ValueType, KeyOfValue> _t;    
};  
// set的基本框架
template<class K>                                                                                                             
class MySet          
{                    
    typedef K ValueType;      
    struct KeyOfValue      
    {                  
      const K& operator()(const ValueType& k)      
      {                                        
        return k;      
      }            
    };    
  private:             
    RBTree<K, ValueType, KeyOfValue> _t;    
 };                                                                              


2. map、set和红黑树之间的关系


map、set都有的接口包括begin()、end()、empty()、size()、Find、Insert等等,看到上面这些接口有没有觉得很熟悉?没错,这些都是上面红黑树已经实现了的接口,map和set因为底层是红黑树所以可以直接调用这些接口,至于有多直接,从它们两个的完整代码里可见一斑。

map和set相当于红黑树的的一件衣服,对内把红黑树装饰和保护了起来,对外使得使用者操作红黑树更加方便、容易理解。


3. map、set完整代码


map

这里要注意一下 [ ] 这个操作符的重载,这是map独有的,底层通过封装Insert实现。方括号内我们传入Key,如果存在的话返回Value的引用;如果不存在,则新插入这个节点,它的Value为对应类型的一个匿名对象,调用其默认构造函数。即该操作符的功能是无论如何都返回给你对应key的value。

c07d52a394e54b7b9ff3124c62be8315.png

template<class K, class V>
class MyMap
{
    typedef pair<K, V> ValueType;
    struct KeyOfValue 
    {
      const K& operator()(const ValueType& v)
      {
        return v.first;
      }
    };
  public:
    typedef typename RBTree<K, ValueType, KeyOfValue>::Iterator Iterator;
    typedef typename RBTree<K, ValueType, KeyOfValue>::const_Iterator const_Iterator;
    pair<Iterator, bool> Insert(const ValueType& v)
    {
      return _t.Insert(v);
    }
    Iterator Begin()
    {
      return _t.Begin();
    }
    const_Iterator Begin() const
    {
      return _t.Begin();
    }
    Iterator End()
    {
      return  _t.End();
    }
    const_Iterator End() const
    {
      return  _t.End();
    }
    size_t Size() const
    {
      return _t.Size();
    }
    bool Empty() const
    {
      return  _t.Empty();
    }
    Iterator Find(const K& k)
    {
      return _t.Find(k); 
    }
    const_Iterator Find(const K& k) const
    {
      return _t.Find(k); 
    }
    V& operator[](const K& k)
    {
      pair<Iterator, bool> ret=_t.Insert(make_pair(k, V()));
      return ret.first->second; 
    }
  private:
    RBTree<K, ValueType, KeyOfValue> _t;
};

set

template<class K>
class MySet
{
  typedef K ValueType;
  struct KeyOfValue
  {
    const K& operator()(const ValueType& k)
    {
      return k;
    }
  };
  public:
    typedef typename RBTree<K, ValueType, KeyOfValue>::Iterator Iterator;
    typedef typename RBTree<K, ValueType, KeyOfValue>::const_Iterator const_Iterator;
    pair<Iterator, bool> Insert(const ValueType& k)
    {
       return _t.Insert(k);
    }
    Iterator Begin()
    {
      return _t.Begin();
    }
    const_Iterator Begin() const
    {
      return _t.Begin();
    }
    Iterator End()  
    {
      return _t.End();
    }
    const_Iterator End() const 
    {
      return _t.End();
    }
    bool Empty() const 
    {
      return _t.Empty();
    }
    size_t Size() const 
    {
      return _t.Size();
    }
    Iterator Find(const K& k)
    {
      return _t.Find(k);
    }
    const_Iterator Find(const K& k) const
    {
      return _t.Find(k);
    }
  private:
    RBTree<K, ValueType, KeyOfValue> _t;
};


相关文章
|
27天前
|
存储 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个高频单词等问题的解决方案。
34 6
【数据结构】map&set详解
|
29天前
|
存储 缓存 Java
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
31 1
|
2月前
|
算法
你对Collection中Set、List、Map理解?
你对Collection中Set、List、Map理解?
35 5
|
2月前
|
存储 JavaScript 前端开发
js的map和set |21
js的map和set |21
|
2月前
|
存储 前端开发 API
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
该文章详细介绍了ES6中Set和Map数据结构的特性和使用方法,并探讨了它们在前端开发中的具体应用,包括如何利用这些数据结构来解决常见的编程问题。
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
|
3月前
|
存储 安全 Java
java集合框架复习----(4)Map、List、set
这篇文章是Java集合框架的复习总结,重点介绍了Map集合的特点和HashMap的使用,以及Collections工具类的使用示例,同时回顾了List、Set和Map集合的概念和特点,以及Collection工具类的作用。
java集合框架复习----(4)Map、List、set
|
3月前
|
Java
【Java集合类面试二十二】、Map和Set有什么区别?
该CSDN博客文章讨论了Map和Set的区别,但提供的内容摘要并未直接解释这两种集合类型的差异。通常,Map是一种键值对集合,提供通过键快速检索值的能力,而Set是一个不允许重复元素的集合。
|
3月前
|
存储 JavaScript 前端开发
ES6新特性(四): Set 和 Map
ES6新特性(四): Set 和 Map
|
3月前
|
存储 Java 索引