模拟实现map和set
- 前言
- 正式开始
- 简单框架
- data的比较
- 迭代器
- operator++
- operator-\-
- [ ]重载
前言
本篇以前一篇红黑树模拟实现插入功能为基础:【C++】红黑树模拟实现插入功能(包含旋转和变色)
本篇中不会再讲解关于旋转和变色的知识。只是对于红黑树进行简单的封装。
如果你在此之前已经对于红黑树的旋转和变色很了解了,就不需要再看前一篇了,但若没了解过的话,建议先看看前一篇,前一篇中的内容懂了才能看下面的内容。
我先将前一篇中实现的代码放这,各位可以先不看,我讲到要修改的时候再看也不迟。
#pragma onceenum color
{RED,BLACK
};template<class K, class V>
struct RBTreeNode
{RBTreeNode(const pair<K, V>& kv = make_pair(K(), V())):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv){}RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;pair<K, V> _kv;color _col;
};template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;public:bool Insert(const pair<K, V>& kv){// 树若为空,就让根直接指向新节点,再让根节点的颜色变为黑色if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}// 找到合适的插入位置Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else{return false;}}// 插入后再连接起来cur = new Node(kv);if (kv.first < parent->_kv.first)parent->_left = cur;else if (kv.first > parent->_kv.first)parent->_right = cur;elseassert(false);cur->_parent = parent;// 插入结束,开始调整树结构cur->_col = RED; // 插入节点一定要为红色while (parent && parent->_col == RED) // 父节点存在且为红时才需要调整。{// 当父节点为红色时,爷爷节点一定为黑色Node* grandParent = parent->_parent;assert(grandParent);assert(grandParent->_col == BLACK);Node* uncle = nullptr;if (parent == grandParent->_left)uncle = grandParent->_right;elseuncle = grandParent->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandParent->_col = RED;cur = grandParent;parent = cur->_parent;}else // uncle不存在 或 uncle存在且为黑{if (parent == grandParent->_left){if (cur == parent->_left) // 单边 左左 ==》右单旋{RotateR(grandParent);parent->_col = BLACK;grandParent->_col = RED;}else // 左 右 ==》左右双旋{RotateL(parent);RotateR(grandParent);cur->_col = BLACK;grandParent->_col = RED;}}else // parent == grandParent->_right{if (cur == parent->_right) // 单边 右右 ==》左单旋{RotateL(grandParent);parent->_col = BLACK;grandParent->_col = RED;}else // 右左 ==》右左双旋{RotateR(parent);RotateL(grandParent);cur->_col = BLACK;grandParent->_col = RED;}}// 只要旋转过后就平衡了break;}}_root->_col = BLACK;return true;}void InOrder(){_InOrder(_root);cout << endl;}bool IsBalance(){// 空树是平衡的if (_root == nullptr)return true;// 根为黑(第一条)if (_root->_col == RED)return false;// 找到最左边路径中黑色节点个数// 以该个数为基准,用来比较其他路径的黑色节点Node* cur = _root;int blackNum = 0;while (cur){if (cur->_col == BLACK)++blackNum;cur = cur->_left;}// 判断二三四条return _IsBalance(_root, 0, blackNum);}private:bool _IsBalance(Node* root, int blackCount, int& blackNum){if (root == nullptr){// 判断当前路径黑节点个数是否与其他相同if (blackCount != blackNum)return false;elsereturn true;}// 是黑色节点就让blackCount++if (root->_col == BLACK)++blackCount;// 不能有连续黑色节点if (root->_col == RED && root->_parent->_col == RED)return false;// 节点非黑即红if (root->_col != BLACK && root->_col != RED)return false;// 继续左右树return _IsBalance(root->_left, blackCount, blackNum)&& _IsBalance(root->_right, blackCount, blackNum);}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << ":::" << root->_kv.second << endl;_InOrder(root->_right);}void RotateL(Node* parent){Node* ppNode = parent->_parent;Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;parent->_parent = subR;if (ppNode){if (ppNode->_left == parent)ppNode->_left = subR;elseppNode->_right = subR;subR->_parent = ppNode;}else{_root = subR;subR->_parent = nullptr;}}void RotateR(Node* parent){Node* ppNode = parent->_parent;Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;subL->_right = parent;parent->_parent = subL;subL->_parent = ppNode;if (ppNode){if (ppNode->_left == parent)ppNode->_left = subL;elseppNode->_right = subL;}else{_root = subL;subL->_parent = nullptr;}}private:Node* _root = nullptr;
};
正式开始
首先,STL库中map和set底层就用的是红黑树,但是map是k/v模型的,set是k模型的,库中是实现了两棵红黑树吗?
不是的,STL库是非常注重代码的复用性的,不会说实现两棵红黑树的。
我们自己模拟实现之前先来看看STL库中是样搞的:
对于map和set另个头文件而言:
对于stl_tree.h而言:
红黑树的节点颜色:
这里用的是bool类型的,0表示RED,1表示BLACK。我前一篇的模拟实现中是用枚举来搞的,和这里不太一样,但是都可以。树节点:
库里面是先实现了一个除存放值以外的指针等节点内容的结构体__rb_tree_node_base,然后再搞了一个结构体__rb_tree_node来继承前一个结构体,在__rb_tree_node内部定义值域,并用模版来使这个值域泛型化,从而使得当模版参数传的是k模型的时候就是set,传pair的时候就是map。树:
这样就实现出了一棵泛型结构的rbtree,通过不同实例化参数,实现出map和set。
对于stl_set.h和stl_map.h而言:
这样就可以动手搞了。
为了和库中的map和set区别,我就将模拟实现的文件命名为 Map.h 和 Set.h 。
简单框架
先把红黑树改改。
我前面直接模拟实现的是 key/value 模型的,但是set是k模型的,所以要将存储的数据类型改成模版,而非直接是pair。那么我就直接用T来表示了。
树节点:
树:
要改的地方不多,就是把所有原来用到模版V的地方改为T,然后将用到kv的地方改为data就好了。
上面我还圈出了用data直接比较的地方,这里直接用大于小于号进行比较是错误的,因为不知道data的类型是key还是key/value,所以直接比的话就出问题了。库中虽然重载了pair的大于小于号,但是并不是我们想用的,我们是想直接比较first就行了, 库中的是比较完first还会比second,所以我们得另寻他路。但是这里还不能讲,得等会在Map.h和Set.h中写点东西才能改。
先给Map和Set打框架:
Map
Set
可能有同学要问,为啥还要有第一个模版参数K呢?
因为插入元素或者查找元素的时候都需要用到key,而当我们只搞一个V时,就会导致map传
pair的时候没法得到key的类型而导致无法比较。
我刚刚也提到了直接用data比较会导致错误,现在我们来解决一下。
data的比较
我们可以参考一下库中是怎么搞的。
库中是用第三个模版参数来解决这个问题的,库里面的原理就不说了,我这里直接实现。
第三个参数是仿函数,KeyOfValue,意思就是提取中value中的key,对于set而言可以直接接将仿函数的返回值给其value,因为set的value就是key。而map则需要返回其元素对应pair
中的key。
所以对于set而言:
对于map而言:
上面传KeyOfValue的用法是将data传给()重载,map的data就是pair,set的data就是key,然后就返回对应的key值。
然后再改一下红黑树中用到data的地方:
先在用到data比较的函数中定义一个KeyOfValue的对象:
然后再给每一个用到data且进行比较的地方都添上kov()
这样就好了。
我们在map和set中封装一下insert:
测试一下:
map:
set:
迭代器
上面我们没法直接打印信息,只能通过调试简单看一下,因为迭代器还没实现。
下面就搞搞迭代器。
还是,红黑树的迭代器就是map和set的迭代器。而红黑树迭代器的实现可以参考参考链表的迭代器。模版参数也是T, Ref, Ptr这三个。
基本框架:
迭代器,无非那几个功能。
我们这里先实现一下*、->、==、!=。
==传参的时候要传一个迭代器,类型太长了,重命名一下:
然后在RBTree中封装一下:
写begin和end时要确定一下begin和end分别指向哪里。
库中是这样搞的:
上面的header就相当于是双向带头循环链表中的哨兵位头结点,begin就是树最左侧的节点,end就是树最右侧的节点。begin的话就是header->_left,end就是header->_right。
但是我就不搞那个header了,不搞也是可以实现的:
那么这里begin就是最左侧的节点,end给空指针就行。
在Map中我们需也要封装一下其迭代器:
注意上面用到了typename关键字,就是为了标识出红黑树中的iterator是一个类型名,而非静态成员,因为静态成员也可直接通过类域访问。
还需要加上begin和end:
然后距离迭代器遍历整棵树还差一步,就是++和--。其实++就够了。
operator++
这里要稍微想一想。
红黑树遍历就指的是中序遍历,这样打印出来的东西是有序的。
但是我们这里不是遍历了,而是访问一个节点一个节点的走。怎样实现呢?
想一想,中序遍历,访问到一个节点是,该节点的左子树一定已经访问过了,此时访问该节点之后,就要访问右子树了。
那么右子树可以分两种情况。
非空
非空的话,按照中序遍历的顺序,应该是直接跑到右子树中最左边的节点。
空
空的话,按照中序遍历的顺序,应该跑到其祖先路径上未被访问过的节点。如果祖先路径都被访问过了,那么就是这棵树已经遍历完了。
想要控制这一点的话,就得不断向上寻找到一个祖先里面孩子不是祖先的右子树的那个祖先节点。因为左根右的顺序,往上寻找的话,根的右一定已经访问过了,所以就是要找根的左的节点。
.
如上图中的7。5、6均已在7之前访问过了,当前为7的话,++就应该跑到8的位置,也就是说不断向祖先节点寻找,7是6的右,6被访问过了,继续看8,6是8的左,8未被访问,就该跑到8的位置了。
.
而图中的15,一路向上,都是父的右,直到8上面为空时,找不到符合条件的祖先节点了。此时就应该停止,operator++返回空指针对应的节点即可。
那我们就按照上面的逻辑实现一下:
测试:
再来看看set:
operator--
--的话,就是++倒着来就行。
逻辑反过来,看左子树就行。
实现一下:
[ ]重载
再来说一下[ ]重载。
这个对于map来说非常有用。也只有map能用,前面map和set介绍的那一篇我也讲了,[ ]的返回值,这里不再细说了,不懂的同学点传送门:【C++】STL map和set用法基本介绍。
首先我们要把红黑树中的insert返回值改一下。改成pair<iterator, bool>,然后内部返回值的细节再改改:
然后map和set中的封装也要改:
然后再在map中重载一下[ ]。
测试:
迭代器走一遍:
范围for走一遍:
erase还是不讲,本篇就只讲一下插入对应的封装。迭代器中有些功能也可以实现但这里没有给,比如说后置++、--等等。红黑树中的find等。如果感兴趣的同学可自行查找资料进行学习。
到此结束。。。