当前位置: 首页 > news >正文

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

本篇我们要用自己实现的红黑树去模拟实现map和set。

红黑树实现详解在:【C++】红黑树的实现详解

1. 调整之前实现的红黑树的insert

1.1 整体框架的搭建

新建两个头文件,Mymap.hMyset.h ,一个源文件 test.cpp ,然后把之前实现的红黑树拷贝一份过来。

为了和库里面的一些东西区分开,我们还是把所有自己实现的内容都放在自己的命名空间里。

Mymap.h中搭建框架。key参数就⽤K,value参数就⽤V。

#include "RBTree.h"
namespace lyj
{template<class K, class V>class map{private:RBTree<K, pair<K, V>> _t;};
}

Myset.h中搭建框架。

#include "RBTree.h"
namespace lyj
{template<class K>class set{private:RBTree<K, K> _t;};
}

直接对这个 RBTree.h 进行修改。 

首先就是把模板参数改掉,红⿊树中的数据类型我们使⽤T

template <class T>
struct RBTreeNode
{T _data;RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Colour _col;RBTreeNode(const T& data):_data(data),_left(nullptr),_right(nullptr),_parent(nullptr){}
};

key参数就⽤K,这里的T就是决定到底是set还是map的。

 如果我们传一个K过去就是set,传pair过去就是map。

1.2 insert的修改

因为我们把红⿊树中的数据类型T来表示了,也就是_data,这个_data是一个泛型,可能是set的K,可能是map的pair,用以前的逻辑就不能满足这个比较。

 这个_kv换成_data也没用,不适用于set的K场景,并且pair自身支持的比较方法也不是我们想要的,我们需要任何时候都只比较K。

此时我们就需要实现一个仿函数。就是取出K来,set中就取K,map中就取first。

Myset.hset类里实现仿函数Set_Key_Of_T

struct Set_Key_Of_T
{const K& operator()(const K& key){return key;}
};

set里这个仿函数就是给 key直接返回key就行。

Mymap.hmap类里实现仿函数Map_Key_Of_T

struct Map_Key_Of_T
{const K& operator()(const pair<K, V>& kv){return kv.first;}
};

map的仿函数就是返回pair里的first。

有仿函数之后,红黑树的模板参数也要多加一个了,叫KeyOfT。

template<class K, class T, class KeyOfT>
class RBTree
{//...
}

 insert里的比较逻辑就要要成用这个仿函数写的逻辑。

map的find返回的是一个pair,这个pair的first是一个迭代器,second是一个bool值,所以这里的返回值也要修改一下。

pair<Iterator, bool> insert(const T& data)
{
}

 插入成功,就返回新插入的值的迭代器和true,插入失败就返回已经存在的这个值的迭代器和false。所以整体代码如下。

pair<Iterator, bool> insert(const T& data)
{if (_root == nullptr){_root = new Node(data);_root->_col = BLACK;  //根节点为黑色return make_pair(Iterator(_root, _root), true);}Node* parent = nullptr;Node* cur = _root;KeyOfT com; //仿函数while (cur){if (com(cur->_data) > com(data)){parent = cur;cur = cur->_left;}else if (com(cur->_data) < com(data)){parent = cur;cur = cur->_right;}else return make_pair(Iterator(cur, _root), false);}cur = new Node(data);Node* newnode = cur; //记录新插入的节点cur->_col = RED;  //新插入节点为红色if (com(cur->_data) < com(parent->_data)){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;while (parent && parent->_col == RED){Node* grandfather = parent->_parent;if (parent == grandfather->_left) //u在右{Node* uncle = grandfather->_right;if (uncle && uncle->_col == RED) //u存在且为红{parent->_col = BLACK; //u和p变黑uncle->_col = BLACK;grandfather->_col = RED;//g变红cur = grandfather; //继续向上更新parent = cur->_parent;}else //u不存在 或 u存在且为黑{if (cur == parent->_left) //单旋{rotateR(grandfather);//以g为旋转点右旋parent->_col = BLACK;  //变色grandfather->_col = RED;}else //双旋{rotateL(parent); //先对p左旋rotateR(grandfather);//再对g右旋//变色cur->_col = BLACK;grandfather->_col = RED;}break;}}else //u在左{Node* uncle = grandfather->_left;if (uncle && uncle->_col == RED) //u存在且为红{parent->_col = BLACK; //p和u变黑uncle->_col = BLACK;grandfather->_col = RED;//g变红cur = grandfather; //继续向上更新parent = cur->_parent;}else //u不存在 或 存在且为黑{if (cur == parent->_right) //单旋{rotateL(grandfather);//以g为中心左旋parent->_col = BLACK; //p变黑grandfather->_col = RED;//g变红}else //双旋{rotateR(parent);//先以p为中心右旋rotateL(grandfather);//再以g为中心左旋cur->_col = BLACK; //c变黑grandfather->_col = RED;//g变红}break;}}}_root->_col = BLACK;return make_pair(Iterator(newnode, _root), true);
}

上面的代码相比之前实现的insert,只有插入部分的代码修改了,旋转部分没做修改。

然后我们用set的insert测试一下。

Myset.h中加上insert的函数,在set类public实现。

public:pair<iterator, bool> insert(const K& key) //插入{return _t.insert(key);}
private:RBTree<K, K, Set_Key_Of_T> _t;

 Mymap.h中加上insert的函数,在map类public实现。

public:pair<iterator, bool> insert(const pair<K, V>& kv) //插入{return _t.insert(kv);}
private:RBTree<K, pair<K, V>, Map_Key_Of_T> _t;

test.cpp中测试。如果没报错,目前这个insert就是对的。

#include "Myset.h"
#include "Mymap.h"
int main()
{lyj::set<int> s; //用自己实现的sets.insert(5);s.insert(3);s.insert(2);s.insert(4);s.insert(1);return 0;
}

2. iterator 迭代器的实现

2.1 部分运算符重载

这里需要同时考虑普通的迭代器和const迭代器,所以还是要一个类模板来实现。

template<class T, class ref, class ptr>
struct RBTree_Iterator
{typedef RBTreeNode<T> Node;typedef RBTree_Iterator<T, ref, ptr> Self;};

 还需要一个节点的指针,并写一个构造函数。

template<class T, class ref, class ptr>
struct RBTree_Iterator
{typedef RBTreeNode<T> Node;typedef RBTree_Iterator<T, ref, ptr> Self;Node* _node;RBTree_Iterator(Node* node):_node(node){}
};

然后还是在类里实现一下operator*,  operator->,  operator!= 和 operator== 这4个运算符重载。

ref operator*()
{return _node->_data;
}ptr operator->()
{return &_node->_data;
}bool operator!=(const Self& s) const
{return _node != s._node;
}bool operator==(const Self& s) const
{return _node == s._node;
}

然后在RBTree类里在重命名一下。

typedef typename RBTree_Iterator<T, T&, T*> Iterator;
typedef typename RBTree_Iterator<T, const T&, const T*> const_Iterator;

2.2 迭代器

2.2.1 begin和end

以下图为例,map和set的迭代器⾛的是中序遍历,左⼦树->根结点->右⼦树,那么 begin() 会返回 中序第⼀个 结点的iterator也就是10 所在结点的迭代器。

end()如何表⽰呢?图中,当it指向50时,++it时,50是40的右,40是30的右,30是18的右,18
到根没有⽗亲,没有找到孩⼦是⽗亲左的那个祖先,这是⽗亲为空了,那我们就把it中的结点指针
置为nullptr,我们⽤ nullptr去充当end

RBTree.hRBTree类中public实现。 

Iterator Begin()
{Node* cur = _root;while (cur && cur->_left){cur = cur->_left;  //找最左节点}return Iterator(cur);
}const_Iterator Begin() const //const迭代器
{Node* cur = _root;while (cur && cur->_left){cur = cur->_left;  //找最左节点}return Iterator(cur);
}
Iterator End()
{return Iterator(nullptr);
}const_Iterator End() const //const迭代器
{return Iterator(nullptr);
}

Myset.hset类里要封装一下。

public:typedef typename RBTree<K, K, Set_Key_Of_T>::Iterator iterator;typedef typename RBTree<K, K, Set_Key_Of_T>::const_Iterator const_iterator;iterator begin(){return _t.Begin();}iterator end(){return _t.End();}const_iterator begin() const  //const迭代器{return _t.Begin();}const_iterator end() const{return _t.End();}

Mymap.hmap类里也要封装一下。

public:typedef typename RBTree<K, pair<K, V>, Map_Key_Of_T>::Iterator iterator;typedef typename RBTree<K, pair<K, V>, Map_Key_Of_T>::const_Iterator const_iterator;iterator begin(){return _t.Begin();}iterator end(){return _t.End();}const_iterator begin() const  //const迭代器{return _t.Begin();}const_iterator end() const{return _t.End();}

2.2.2 ++和--

迭代器++的核⼼逻辑就是不看全局,只看局部,只考虑当前中序局部要访问的下⼀个结点。
  • 迭代器++时,如果it指向的结点的右⼦树不为空,代表当前结点已经访问完了,要访问下⼀个结点是右⼦树的中序第⼀个,⼀棵树中序第⼀个是最左结点,所以直接找右⼦树的最左结点即可。
  • 迭代器++时,如果it指向的结点的右⼦树空,代表当前结点已经访问完了且当前结点所在的⼦树也访问完了,要访问的下⼀个结点在当前结点的祖先⾥⾯,所以要沿着当前结点到根的祖先路径向上找。
如果当前结点是⽗亲的左,根据中序左⼦树->根结点->右⼦树,那么下⼀个访问的结点就是当前结点的⽗亲。
如下图:it指向25,25右为空,25是30的左,所以下⼀个访问的结点就是30。

如果当前结点是⽗亲的右,根据中序左⼦树->根结点->右⼦树,当前当前结点所在的⼦树访问完了,当前结点所在⽗亲的⼦树也访问完了,那么下⼀个访问的需要继续往根的祖先中去找,直到找
到孩⼦是⽗亲左的那个祖先就是中序要问题的下⼀个结点。
如下图:it指向15,15右为空,15是10 的右,15所在⼦树话访问完了,10所在⼦树也访问完了,继续往上找,10是18的左,那么下⼀个访问的结点就是18。

RBTree.hRBTree_Iterator类中实现。

 右不为空,中序的下一个访问的节点是右子树的最左(最小)节点。

Self& operator++() //前置++
{if (_node->_right) //右不为空{//右子树最左结点就是中序第⼀个Node* min = _node->_right;//先指向右树while (min->_left){min = min->_left;//直接往左一直找}_node = min;}else //右为空{}
}

右为空,访问祖先里面孩子是祖先左孩子的那个祖先。

else //右为空
{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right) //parent可能会走到空{cur = parent;parent = parent->_parent;}//当cur不是parent的右子树时,就是parent的左子树,就是++要访问的下一个节点_node = parent; 
}
return *this;

如果走到了中序的最后一个节点,上面的代码是能让_node为空的,不需要分情况处理。

先在test.cpp中测试一下operator++。

用map测试这个++。

#include "Myset.h"
#include "Mymap.h"
int main()
{lyj::map<string, string> m; //用自己实现的mapm.insert({ "hello", "你好" });m.insert({ "left", "左" });m.insert({ "right", "右" });m.insert({ "word", "单词" });auto it = m.begin();while (it != m.end()){cout << it->first << ":" << it->second << endl;++it;}return 0;
}

看起来没什么问题。 

按道理来说, set和map的key都不允许修改 map的value可修改,但是我们目前实现的迭代器,可以改动任意值,如下。
lyj::set<int> s;
s.insert(5);
s.insert(2);
s.insert(3);
s.insert(1);
lyj::set<int>::iterator s_it = s.begin();
*s_it += 10; //修改迭代器
for (int e : s)
{//e += 10;cout << e << " ";
}
cout << endl;

lyj::map<string, string> m; //用自己实现的map
m.insert({ "hello", "你好" });
m.insert({ "left", "左" });
m.insert({ "word", "单词" });
auto it = m.begin();
while (it != m.end())
{it->first += 'x'; //修改keyit->second += 'y';//修改valuecout << it->first << ":" << it->second << endl;++it;
}

解决这种情况的方法非常简单。

对于set,我们把set的第⼆个模板参数改成const K即可。

private:RBTree<K, const K, Set_Key_Of_T> _t; 

改了这里,我们相关typedef的地方也要修改。

public:typedef typename RBTree<K, const K, Set_Key_Of_T>::Iterator iterator;typedef typename RBTree<K, const K, Set_Key_Of_T>::const_Iterator const_iterator;

改了之后,再运行上面的代码就会报错。set迭代器是不支持修改的,现在就是不支持。

对于map,解决方法差不多,但不完全一样。我们把map的第⼆个模板参数pair的第⼀个参数改成const K即可。

private:RBTree<K, pair<const K, V>, Map_Key_Of_T> _t;

 同样的,相关typedef的地方也要修改。

public:typedef typename RBTree<K, pair<const K, V>, Map_Key_Of_T>::Iterator iterator;typedef typename RBTree<K, pair<const K, V>, Map_Key_Of_T>::const_Iterator const_iterator;

 改了之后,再运行上面map的代码就会报错。此时就是map的iterator不⽀持修改key但是可以修改value。

 

迭代器-- 的实现跟++的思路完全类似,逻辑正好反过来即可,因为他访问顺序是 右⼦树->根结点->
左⼦树 ,具体参考下⾯代码实现。
Self& operator--()
{if (_node->_left){// 左子树不为空,中序左子树最后⼀个Node* rightMost = _node->_left;while (rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else{// 孩⼦是父亲右的那个祖先Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = parent;parent = cur->_parent;}_node = parent;}return *this;
}

但是这里有一个特殊情况要处理,就是对end()进行--。因为我们的end是nullptr,不能对nullptr进行--的操作。解决方法如下。

如果_node是nullptr,也就是如果_node是end,对end进行--,我们就去找树的最右节点。从根节点找最右节点,但是我们不知道根是什么,所以这里还要增加一个成员变量,_root。

RBTree_Iterator 内改动。

Node* _node;
Node* _root; //增加根节点RBTree_Iterator(Node* node, Node* root):_node(node), _root(root)
{}

因为这里多加了一个参数,所以别的迭代器也要改,都要传_root过去。

RBTree类内改动。

Iterator Begin()
{Node* cur = _root;while (cur && cur->_left){cur = cur->_left;  //找最左节点}return Iterator(cur, _root);
}Iterator End()
{return Iterator(nullptr, _root);
}const_Iterator Begin() const
{Node* cur = _root;while (cur && cur->_left){cur = cur->_left;  //找最左节点}return Iterator(cur, _root);
}const_Iterator End() const
{return Iterator(nullptr, _root);
}

然后我们里看--的完整代码。在RBTree_Iterator内实现。

Self& operator--()
{if (_node == nullptr) // end(){// --end(),特殊处理,走到中序最后⼀个结点,整棵树的最右结点Node* rightMost = _root;while (rightMost && rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else if (_node->_left){// 左子树不为空,中序左子树最后⼀个Node* rightMost = _node->_left;while (rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else{// 孩⼦是父亲右的那个祖先Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = parent;parent = cur->_parent;}_node = parent;}return *this;
}

test.cpp中测试一下, 倒序打印。

void Print(const lyj::set<int>& s)
{lyj::set<int>::const_iterator it = s.end();while (it != s.begin()){--it; cout << *it << " ";}cout << endl;
}int main()
{lyj::set<int> s;s.insert(5);s.insert(2);s.insert(3);s.insert(1);Print(s);return 0;
}

2.3 operator[]

 关于operator[]的功能及如何实现的,在【C++】map和multimap的常用接口详解 有详细的介绍,这里直接给出代码。在Mymap.hmap类里实现。

V& operator[](const K& key)
{pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;
}

test.cpp中测试一下。

int main()
{lyj::map<string, string> m; //用自己实现的mapm.insert({ "hello", "你好" });m.insert({ "right", "右边" });m.insert({ "word", "单词" });m["right"] = "对的"; //修改m["left"] = "左边"; //插入+修改m["cat"]; //插入auto it = m.begin();while (it != m.end()){cout << it->first << ":" << it->second << endl;++it;}return 0;
}

 

3.析构

在 RBTree.h RBTree类private实现。

void Destroy(Node* root)
{if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;
}

 在 RBTree.h RBTree类public实现。

~RBTree()
{Destroy(_root);
_   root = nullptr;
}

map和set的主要实现就介绍到这里,还有别的接口可自行实现 ,我们下篇再见~

http://www.xdnf.cn/news/1038601.html

相关文章:

  • [Linux入门] Linux磁盘管理与文件系统
  • YOLOv3 中的 IoU 计算详解
  • 在Ubuntu linux终端写文件的方法
  • FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
  • 【Zephyr 系列 25】多芯片协同设计:主控 + BLE + LoRa 芯片的统一调度与消息系统
  • 什么是泛型,如何使用它?
  • 动态组件(component)的高级使用
  • PL端DDR3读写(1)
  • 转换专家从格式转换到创意剪辑的全链路解决方案
  • AIGC 基础篇 Python基础(练习1)
  • 板凳-------Mysql cookbook学习 (十--6)
  • Python6.14打卡(day46)
  • StampedLock入门教程
  • 面试问题总结——关于C++(四)
  • 【卫星通信】3GPP标准提案:面向NB-IoT(GEO)场景的IMS信令优化方案-降低卫星通信场景下的语音呼叫建立时延
  • ELK日志文件分析系统——L(Logstash)
  • Flutter 状态管理与 API 调用的完美结合:从理论到实践
  • python实战:使用Python合并PDF文件
  • pyqt5,python开发软件,文件目录如何设置,如何引用,软件架构如何设计
  • 洛谷 P5711:闰年判断
  • 基于Python学习《Head First设计模式》第十一章 代理模式
  • 「Linux中Shell命令」Shell常见命令
  • Vue 3 砸金蛋互动抽奖游戏
  • Redis事务与驱动的学习(一)
  • 出现端口占用,关闭端口进程命令
  • Redis三种集群概述:主从复制、哨兵模式与Cluster模式
  • MySQL 究极奥义·动态乾坤大挪移·无敌行列转换术
  • SSH参数优化与内网穿透技术融合:打造高效远程访问解决方案
  • Android 获取签名 keystore 的 SHA1和MD5值
  • transactional-update原子性更新常用命令