当前位置:首页 » 《随便一记》 » 正文

C++:用红黑树封装map与set-1

12 人参与  2024年11月28日 16:01  分类 : 《随便一记》  评论

点击全文阅读


在这里插入图片描述

文章目录

前言一、STL源码分析二、红黑树的构建三、map与set整体框架的搭建与解析四、如何取出进行比较?1. met与set的数据是不同的2. 取出数据进行比较1)问题发现2)仿函数解决 五、封装插入六、迭代器的实现1. operator* 与operator->2. operator!=3. operator++4. operator- -5. 套用普通迭代器 七、const迭代器八、查找总结


前言

之前我们学习了红黑树的实现,现在我们一起来看一看如何使用红黑树封装出set与map~~~


一、STL源码分析

我们一起来分析分析STL源码,看一看库中是如何实现的??

在这里插入图片描述
首先,库里面stl_set与stl_map中,

对于set来说,key_typekeyvalue_type也是key,也就是说set是一个rbTree<Key, Key>的模型。

对于map来说,key_typekey,但是value_typepair<const key, T>,也就是说map是一个rbTree<Key, pair<Key, Value>>的模型。

我们再来看一下rb_tree的结构:

在这里插入图片描述

rb_tree中,前两个参数是<key, value>,而__rb_tree_node<value>里面的参数传的是value,因此我们可以总结出,这里map的结点中存储的是pair<key, value>,而set的结点中存储的是key

在这里插入图片描述

那么到底为什么是这样的结构呢?
我们将在下面的讲解中逐一解释?


二、红黑树的构建

对于红黑树的构建,J桑之前的文章有详细的讲解,因此这里就直接给出代码啦,还不清楚的观众老爷门可以点击下方的传送门???

深入探索:C++红黑树原理与实现

当然,我们之前红黑树是默认存储pair,现在要同时满足map与set,因此一些地方需要改变,之后总结的时候会给出完整的更改后的代码,下面讲解也会被提到。

#pragma once#include<iostream>using namespace std;enum Colour{RED,BLACK};template<class K, class V>struct RBTreeNode{pair<K, V> _kv;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;Colour _col;RBTreeNode(const pair<K, V>& kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED){}};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 (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first)   // 大于往右走{parent = cur;cur = cur->_right;}else{return false;}}cur = new Node(kv);// 其他结点初始颜色为红色cur->_col = RED;// 链接if (cur->_kv.first < parent->_kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;// 如果我们parent是黑色,不用处理,就结束了// 情况一:cur为红,parent为红,grandfather为黑,uncle不确定// 用while循环是因为我们要不断的向上调整while (parent && parent->_col == RED){// 首先我们要找到grandfatherNode* grandfather = parent->_parent;// 接下来通过grandfather找到uncle//如果parent是grandfather->_leftif (parent == grandfather->_left){// 说明uncle在右边Node* uncle = grandfather->_right;// uncle存在且为红if (uncle && uncle->_col == RED){// 满足上述情况,开始调整颜色parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续向上调整cur = grandfather;parent = cur->_parent;// 但是走到这里有一个问题,如果当前parent就是根,// 并且parent是红色,我们要把它变为黑色,// 处理方式是:出循环后,暴力将_root->_col变为黑色}else    // uncle不存在 或 uncle存在且为黑色{// 判断要怎样旋转// 右单旋if (cur == parent->_left){//     g//   p// cRotateR(grandfather);// 调整颜色parent->_col = BLACK;grandfather->_col = RED;}else // 左右双旋{//     g//   p//cRotateL(parent);RotateR(grandfather);// 调整颜色cur->_col = BLACK;grandfather->_col = RED;}// 旋转变色完就结束了,这里不加这个也可以,条件判断就会退出break;}}else //(parent == grandfather->_right)  // 如果parent是grandfather->_right{// 说明uncle在左边Node* uncle = grandfather->_left;// uncle存在且为红if (uncle && uncle->_col == RED){// 满足上述情况,开始调整颜色parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续向上调整cur = grandfather;parent = cur->_parent;}else    // uncle不存在 或 uncle存在且为黑色{// 判断要怎样旋转// 左单旋if (cur == parent->_right){// g//  p//       cRotateL(grandfather);// 调整颜色parent->_col = BLACK;grandfather->_col = RED;}else // 右左双旋{// g//  p// cRotateR(parent);RotateL(grandfather);// 调整颜色cur->_col = BLACK;grandfather->_col = RED;}break;}}}// 这里保证根为黑色_root->_col = BLACK;return true;}// 左单旋void RotateL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;// 重新链接parent->_right = curleft;if (curleft) // 如果curleft存在{curleft->_parent = parent;}cur->_left = parent;Node* ppnode = parent->_parent;parent->_parent = cur;if (ppnode == nullptr){_root = cur;cur->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}}// 右单旋void RotateR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;parent->_left = curright;if (curright){curright->_parent = parent;}cur->_right = parent;Node* ppnode = parent->_parent;parent->_parent = cur;if (ppnode == nullptr){cur->_parent = nullptr;_root = cur;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}}// 检查是否构建正确bool CheckColour(Node* root, int blacknum, int benchmark){if (root == nullptr){if (blacknum != benchmark)return false;return true;}if (root->_col == BLACK){++blacknum;}if (root->_col == RED && root->_parent && root->_parent->_col == RED){cout << root->_kv.first << "出现连续红色节点" << endl;return false;}return CheckColour(root->_left, blacknum, benchmark)&& CheckColour(root->_right, blacknum, benchmark);}bool IsBalance(){return IsBalance(_root);}bool IsBalance(Node* root){if (root == nullptr)return true;if (root->_col != BLACK){return false;}// 基准值int benchmark = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK)++benchmark;cur = cur->_left;}return CheckColour(root, 0, benchmark);}private:Node* _root = nullptr;};

三、map与set整体框架的搭建与解析

代码:

namespace jyf{template<class k, class v>class map{public:private:RBTree<k, pair<k, v>> _t;};}
namespace jyf{template<class k>class set{public:private:RBTree<k, k> _t;};}

解析:

首先无论是map还是set都有两个模板参数,第一个是key,这个key是多存的,它的作用体现在像find()这样的函数中,我们后面在做解析。

这里先看第二个参数,传给了RBTreeT,而T就是结点中储存的东西,也就是说,你是set,那么节点中就储存的是key, 你是map,那么节点中就储存的是pair<k, v>

在这里插入图片描述


四、如何取出进行比较?

1. met与set的数据是不同的

我们要明白一个我问题,met与set结点中存储的数据是不一样的,因此,如果节点中还存储的是pair就不对了,因此,我们结点之中存储的数据因该是T类型的数据,如果是set就是key,如果是map就是pair。

在这里插入图片描述


2. 取出数据进行比较

1)问题发现

再insert函数中,我们之前的红黑树是这样实现的,比如这个找到插入位置的逻辑:

在这里插入图片描述
在这张图片里面,我们之前使用pair,但是现在对于map和set存储的数据不同,因此需要用data来比较。

但是!!!
对于map来说,它的value是pair,但是pair的比较逻辑能满足我们的需要吗?

在这里插入图片描述

可以看到,pair的比较逻辑是先比first,first一样就比second,但是,我们这里不需要比较second,key_value的模型中,只需要比较key不同,因此我们需要一种方法,重新定义我们的比较。

怎么重新比较呢?
这里我们通过观察发现,对于set来说,它的data是key,可以直接比较,唯一有问题的是map,因此我们采取的方式是仿函数。


2)仿函数解决

我们可以多定义一个模板参数KeyOfT,这个模板参数用来定义仿函数,他的作用是取出Set或Map中的Key。

对于Set,它的data直接就是key:

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

对于Map,它的data是一个pair,我们需要pair的first,也就是key:

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

在这里插入图片描述

至此,我们在取出数据的时候,只要定义出一个对象,重载operator(),用()将data包起来,就得到了我们想要的数据。

可以说,这里set迁就了map~


五、封装插入

完成了上述步骤,我们就可以实现封装map与set的插入了~

对于set:

bool insert(const K& key){return _t.Insert(key);}

对于map:

bool insert(const pair<K,V>& kv){return _t.Insert(kv);}

插入结果:
在这里插入图片描述

在这里插入图片描述


六、迭代器的实现

要实现迭代器,就要先理解迭代器是怎么用的:
下面是一个模板表示迭代器的使用:

it = s.begin();while (it != s.end()){    cout << *it << endl;    ++it;}

为了实现这个过程,我们需要重载很多东西。

我们先将框架搭出来:

template<class T>struct __TreeIterator{typedef RBTreeNode<T> Node;typedef __TreeIterator<T> Self;Node* _node;__TreeIterator(Node* node):_node(node){}};

1. operator* 与operator->

RBTree中:

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

2. operator!=

bool operator!= (const Self& s){return _node != s._node;}

3. operator++

对于树的迭代器,++与- -就非常重要了,这里有很多坑~

首先对于一个红黑树,他走的是中序的排序,图如下:

在这里插入图片描述

那么,it.begin()是谁呢?
我们说中序是左-根-右,也就是说,begin应该是上图中的1

其次,如果我们进行++操作,迭代器会到那里去呢?
这里分以下几种情况:

如果右不为空

那么下一个访问的,将是右树的最左:

在这里插入图片描述


如果右为空

这里又分两种情况:

cur是parent的左——下一个访问parent
在这里插入图片描述

cur是parent的右——下一个访问没有被访问的祖先
在这里插入图片描述

通过观察这两种情况我们可以发现:
也就是说,当右为空的时候,下一个访问的是孩子是父亲的左侧的那一个祖先。


代码总结:

Self& operator++ (){if (_node->_right != nullptr){Node* curleft = _node->_right;while (curleft->_left){curleft = curleft->_left;}_node = curleft;}else{// 找孩子是父亲左的那个祖先节点,就是下一个要访问的节点Node* cur = _node;Node* parent = _node->_parent;while (parent){if (parent->_left == cur){break;}else{cur = parent;parent = parent->_parent;}}_node = parent;}return *this;}

4. operator- -

原理与++是一样的,只不过原本++的顺序是中序,即左-根-右,- - 是反过来的,因此是右-根-左

Self& operator--(){if (_node->_left){Node* subRight = _node->_left;while (subRight->_right){subRight = subRight->_right;}_node = subRight;}else{// 孩子是父亲的右的那个节点Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = cur->_parent;parent = parent->_parent;}_node = parent;}return *this;}

5. 套用普通迭代器

RBTree中:

经过前面的分析,begin就是树最左边的结点,end我们设置为nullptr。

typedef __TreeIterator<T> iterator;public:iterator begin(){Node* leftMin = _root;while (leftMin && leftMin->_left){leftMin = leftMin->_left;}return iterator(leftMin);}iterator end(){return iterator(nullptr);}

set与map中,我们要封装这个方法:

iterator begin(){return _t.begin();}iterator end(){return _t.end();}

测试普通迭代器:

jyf::map<int, int> m;m.insert(make_pair(1, 1));m.insert(make_pair(3, 3));m.insert(make_pair(2, 2));jyf::map<int, int>::iterator mit = m.begin();while (mit != m.end()){mit->first = 1;mit->second = 2;cout << mit->first << ":" << mit->second << endl;++mit;}cout << endl;for (const auto& kv : m){cout << kv.first << ":" << kv.second << endl;}cout << endl;jyf::set<int> s;s.insert(5);s.insert(2);s.insert(2);s.insert(12);s.insert(22);s.insert(332);s.insert(7);auto it = s.begin();while (it != s.end()){// Ӧ޸if (*it % 2 == 0){*it += 10;}cout << *it << " ";++it;}cout << endl;for (const auto& e : s){cout << e << " ";}cout << endl;

结果为:
在这里插入图片描述


七、const迭代器

我们知道set是不允许修改的,map的key不允许修改,而value允许修改,再通过观察库中的实现,我们可以发现:
在这里插入图片描述

set实现不能修改的原因是——interator迭代器与const_iterator都是const迭代器。

而map实现key不能修改,value可以修改的方法是,在定义map的value的时候,pair<K, V>修改为pair<const K, V>

具体的逻辑我们下一次在进行讲解~


八、查找

查找是通过key来查找的,而不是通过value来查找的,这也就解释了为什么最开始定义模板参数还要多定义一个key。

同样,为了取出对应的值,我们也需要仿函数来包上data。

Node* Find(const K& key){Node* cur = _root;KeyOfT kot;while (cur){if (kot(cur->_data) < key){cur = cur->_right;}else if (kot(cur->_data) > key){cur = cur->_left;}else{return cur;}}return nullptr;}

总结

红黑树在 STL 的应用
set 与 map 的实现:

set 节点存储 key(rbTree<Key, Key> 模型)。

map 节点存储 pair<const Key, T>(rbTree<Key, pair<Key, Value>> 模型)。

rbTree 的设计:
节点使用 __rb_tree_node,value 的具体含义根据容器类型不同而不同。

在这里插入图片描述


点击全文阅读


本文链接:http://zhangshiyu.com/post/193452.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1