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

在这里插入图片描述

文章目录

  • 前言
  • 一、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

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

  1. 如果右不为空

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

在这里插入图片描述


  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://www.rhkb.cn/news/477749.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Perforce《2024游戏技术现状报告》Part3:生成式AI、版本控制、CI/CD等游戏技术的未来趋势与应用

游戏开发者一直处于创新前沿。他们的实践、工具和技术受到各行各业的广泛关注&#xff0c;正在改变着组织进行数字创作的方式。 近期&#xff0c;Perforce发布了《2024游戏技术现状报告》&#xff0c;通过收集来自游戏、媒体与娱乐、汽车和制造业等高增长行业的从业者、管理人…

4-SpringCloud整合服务间的调用即负载均衡

springcloud目录&#xff1a; 1.Spring Cloud简介 2.SpringCloud整合eureka注册中心 3.SpringCloud整合服务注册 4.SpringCloud整合服务间的调用即负载均衡 5.SpringCloud整合Feign调用 6.SpringCloud整合config配置中心 7.SpringCloud整合zuul路由网关 我们复制一个yqx-user服…

Elasticsearch客户端在和集群连接时,如何选择特定的节点执行请求的?

大家好&#xff0c;我是锋哥。今天分享关于【Elasticsearch客户端在和集群连接时&#xff0c;如何选择特定的节点执行请求的&#xff1f;】面试题。希望对大家有帮助&#xff1b; Elasticsearch客户端在和集群连接时&#xff0c;如何选择特定的节点执行请求的&#xff1f; 100…

深入浅出,快速安装并了解汇编语言

1.什么是汇编语言 了解汇编语言需要先从了解机器语言开始&#xff0c;在计算机发展的初期阶段&#xff0c;机器语言是计算机直接理解和执行的二进制代码语言&#xff0c;其核心特点包括直接执行性、资源高效性、学习难度大以及平台依赖性。它主要由指令码构成&#xff0c;这些…

2.2_3 纠错编码—海明码

目录 1、海明码的纠错过程 2、海明距离 3、确认检验码位数 4、确定校验码和数据的位置 5、求出校验码的值 6、检错并纠错 方法一 方法二 1、海明码的纠错过程 2、海明距离 两个合法编码(码字)的对应比特取值不同的比特数称为这两个码字的海明距离(码距)&#xff0c;一…

1992-2021年 各省市县经过矫正的夜间灯光数据(GNLD、VIIRS)区域汇总:省份、城市、区县面板数据

1992-2021年 各省市县经过矫正的夜间灯光数据&#xff08;GNLD、VIIRS&#xff09;区域汇总&#xff1a;省份、城市、区县面板数据 .r.rar https://download.csdn.net/download/2401_84585615/90001905 从1992年至2021年&#xff0c;中国各省份、城市及区县的夜间灯光数据经过…

微信小程序上传微信官方审核流程(1)

1&#xff0c;打开微信开发者工具 2&#xff0c;微信开发者工具右上角有一个上传按钮&#xff0c;点击上传按钮 3&#xff0c;点击完上传按钮会弹出一个上传成功的提示&#xff0c;点击提示框中的确定按钮 4&#xff0c;点击完确定按钮后会显示填写版本好和项目备注 5&#x…

快速获取镜像包的方法

1、当我们需要在无网络的环境中&#xff0c;在Docker环境中安装某个镜像时&#xff0c;需要先下载这个镜像包后&#xff0c;再上传 2、下面以在minio为例 在有网络的电脑中使用使用命令下载 docker pull minio/minio将下载好的tar包保存到指定的目录下 save -o /home/cl/app…

11 —— 打包模式的应用

需求&#xff1a;在开发模式下想让webpack使用style-loader进行css样式的处理&#xff1b;让它把css代码内嵌在js中&#xff1b;在生产模式下提取css代码 —— 判断当前运行命令时所在的环境 方案&#xff1a;借助cross-env全局软件包&#xff0c;设置参数区分打包运行环境 …

docker容器化部署springboot项目

前言 docker安装 下载官网 选择自己的系统 然后安装文档内给的命令按顺序执行即可。设置仓库&#xff0c;安装docker. 一、更换镜像源 一般情况下,docker原本自带的镜像网站不一定连的上,就很容易导致下载镜像失败,因此需要换源. 创建/etc/docker/daemon.json并填入数据…

2024深育杯misc2

题目描述&#xff1a;攻击者远程服务器监听所用的端口是( )&#xff1f;请提交flag&#xff0c;例如端口号为80&#xff0c;则提交Sangfor{80} 附件解压打开是一个raw文件 用volatility3工具查看ip链接信息

UI自动化测试中公认最佳的设计模式-POM

一、概念 什么是POM&#xff1f; POM是PageObjectModule&#xff08;页面对象模式&#xff09;的缩写&#xff0c;其目的是为了Web UI测试创建对象库。在这种模式下&#xff0c;应用涉及的每一个页面应该定义为一个单独的类。类中应该包含此页面上的页面元素对象和处理这些元…

L14.【LeetCode笔记】返回倒数第k个节点

目录 1.题目 2.分析 思路 代码 提交结果 1.题目 面试题 02.02. 返回倒数第 k 个节点 实现一种算法&#xff0c;找出单向链表中倒数第 k 个节点。返回该节点的值。 注意&#xff1a;本题相对原题稍作改动 示例&#xff1a; 输入&#xff1a; 1->2->3->4->5 和 …

linux-进程间通信

进程的通信是两个或多个进程实现数据的交互&#xff0c;让不同的进程看到同一份资源&#xff0c;而这份资源是由操作系统创建管理的。如果让其中一个进程来提供的话会破坏该进程的独立性&#xff0c;因为这个进程内部的数据可以被其他进程看到&#xff0c;那这个独立性就遭到了…

基于阿里云服务器部署静态的website

目录 一&#xff1a;创建服务器实例并connect 二&#xff1a;本地文件和服务器share 三&#xff1a;关于IIS服务器的安装预配置 四&#xff1a;设置安全组 五&#xff1a;建站流程 六&#xff1a;关于备案 一&#xff1a;创建服务器实例并connect 创建好的服务器实例在云…

Java算法OJ(10)哈希表练习

目录 1.前言 2.正文 2.1俩数之和 2.2无重复字符的最长子串 2.3罗马数字转整数 2.4整数转罗马数字 3.小结 1.前言 哈喽大家好吖&#xff0c;今天来分享几道哈希表相关的练习题&#xff0c;操作比较基础但是思想比较重要&#xff0c;另外有许多思路与解法都是学习参照题解…

二叉树:堆的建立和应用

在建立堆之前&#xff0c;我们要知道什么是树和二叉树 树 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个结点组成的一个具有层次关系的集合&#xff0c;之所以把它叫做树&#xff0c;是因为它长得像一棵倒挂的树&#xff0c;也就是根在上面&…

oracle的静态注册和动态注册

oracle的静态注册和动态注册 静态注册&#xff1a; 静态注册 : 指将实例的相关信息手动告知 listener 侦 听 器 &#xff0c; 可以使用netmgr,netca,oem 以及直接 vi listener.ora 文件来实现静态注册&#xff0c;在动态注册不稳定时使用&#xff0c;特点是&#xff1a;稳定&…

postgresql按照年月日统计历史数据

1.按照日 SELECT a.time,COALESCE(b.counts,0) as counts from ( SELECT to_char ( b, YYYY-MM-DD ) AS time FROM generate_series ( to_timestamp ( 2024-06-01, YYYY-MM-DD hh24:mi:ss ), to_timestamp ( 2024-06-30, YYYY-MM-DD hh24:mi:ss ), 1 days ) AS b GROUP BY tim…

调试器 gdb/cgdb 的使用

一. touch mycode.c vim mycode.c cgdb 下载 Ubuntu&#xff1a;sudo apt-get install -y cgdb Centos: sudo yum install -y cgdb Linux 下我们编译好的代码无法直接调试 g/gcc 默认的工作模式是release模式 程序要调试&#xff0c;必须是debug模式&#xff0c;编译时…