【C++笔记】红黑树封装map和set

news2024/12/23 23:24:57

一、map和set的泛型封装逻辑

map和set的底层都是红黑树,所以我们想要用红黑树封装map和set的第一个问题就来了,因为set是key结构而map是key-value结构,怎样用同一个底层结构去封装出两个不同存储结构的容器呢?难道我们要将红黑树的代码复制两份,然后针对性的修改代码吗?

其实并不需要,因为C++有模板啊,我们可以借助C++的模板来帮我们完成一个泛型封装,简单来说就是map和set在上层将底层红黑树所需要存储的数据以模板参数的形式传给底层的红黑树。

具体逻辑如下图:

在上层set和map分别传给红黑树的是K和pair<K, V>,然后在通过红黑树类将参数传给红黑树节点类,最终就决定了红黑树里面存的是什么数据了。

代码:

// Set
template <class K>
class Set {
public :
private :
	RBTree<K> _t;
};


// Map
template <class K, class V>
class Map {
public :
private :
	RBTree<pair<K, V>> _t;
};

// 红黑树类
template <class T>
class RBTree {
public :
	typedef
		RBTree<T> Node;
private :
	Node _root;
};


// 红黑树节点
template <class T>
struct RBTreeNode {
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _val;
	Color _col;
	// 构造
	RBTreeNode(const T& val)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _val(val)
		, _col(RED) // 新增节点要给红色
	{}
};

二、插入中值比较的解决方法

解决了第一个问题,我们就可以实现map和set的插入了,我们知道当下层已经实现了插入逻辑后,上层实现的插入逻辑是很简单的,只需要复用下层的插入即可。

好像这样确实已经可以了,但是我们不能忽略了一个重要的问题,map中存的是一个键值对pair,而我们在插入或查找的时候进行比较都是只针对pair中的K的,也就是pair中的first成员,那pair本身重载的比较方法是否也是只针对fist呢?

我们可以先去查查文档:

查看文档后我们就会发现,pair本身重载的比较方法并不是只针对与first的,它是first和second都会比较到。

所以,如果只用pair原来的比较方法在插入或者是查找的时候就可能出现问题了,所以我们要想个办法统一一下比较方法。

因为pair库中的一个结构,所以我们并不能到它内部去重载它的比较方法,所以我们就需要“提取”出它的first成员。

我们可以创建一个仿函数来做到这个事情,这是因为仿函数也称为函数对象,它是可以作为模板参数传到下层的红黑树的,比较方便。

为了统一,Set和Map都需要一个仿函数,然后传给红黑树:

但是对于Set来说这个KeyOfValue只是走走过场而已,因为它本身就只存储了一个K,完全只是为了配合Map。

然后我们就只需要在所有涉及到值比较的地方套上这个仿函数就能解决问题了:

代码:

// Set
struct KeyOfValue {
	K& operator()(const K& key) {
		return key;
	}
};

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

这样我们就可以放心的完成Map和Set的插入逻辑了:

// Map插入
bool insert(const pair<K, V>& kv) {
	return _t.insert(kv);
}

// Set插入
bool insert(const K& val) {
	return _t.insert(val);
}

我们也可以临时写一个中序遍历来试验一下:

 

三、迭代器的实现

然后我们就需要来实现我们容器必须的迭代器了,其实树形结构的迭代器和链表结构的迭代器是一样的,都是创建一个迭代器类然后封装一个节点指针,只是在迭代器向后何向前移动的时候实现逻辑不同而已:

// 红黑树迭代器类
template <class T>
class RBTreeIterator {
	typedef RBTreeNode<T> Node;
	typedef RBTreeIteraotr<T> iterator;
public :
	// 构造
	RBTreeIterator(Node* node)
		:_node(node)
	{}
	// 星号解引用
	T& operator*() {
		return _node->_val;
	}
	// 箭头解引用
	T* operator->() {
		return &_node->_val;
	}
	// 不等于
	bool operator!=(const iterator& it) {
		return _node != it._node;
	}
	// 前置++
	iterator& operator++() {
		// ……
	}
	// 前置--
	iterator& operator--() {
		// ……
	}
private :
	Node* _node;
};

3.1、迭代器++的实现

然后我们就要来实现迭代器最主要的功能——++操作。

迭代器其实就是根据二叉树的中序遍历来走的,所以我们一开始来找迭代开始的第一个节点,也就是找begin,就是要找到中序遍历的第一个位置。

根据中序遍历的规则我们很容易知道中序遍历的第一个位置就是二叉树中最左的那个节点,所以我们红黑树的begin的实现逻辑就如下:

// 迭代器开始位置
iterator begin() {
	Node* cur = _root;
	// 找到最左节点
	while (cur && cur->_left) {
		cur = cur->_left;
	}
	// 返回使用迭代器的构造即可
	return iterator(cur);
}

 然后结束位置,我们给个空就就行了,因为最后遍历完都会遍历到空:

// 迭代器结束位置
iterator end() {
	return iterator(nullptr);
}

 那我们又怎么从起始位置走到下一个位置呢?我们首先来分析一下中序遍历的逻辑:

我们知道中序遍历的顺序是“左中右”,并且是递归的形式进行的,所以对于每一个节点都会先去遍历它的左子树。由此我们就能得到一个结论:只要当前节点被遍历到那么要么是它没有左子树要么就是它的左子树已经被遍历完了。

所以,如果当前节点的右子树不为空的话。接下来就要遍历到右子树了,而右子树的遍历又是递归“左中右”的形式,所以当前节点的下一个节点就是右子树的最左节点。

这是第一种情况。

那第二种情况就是右子树为空的情况,这个情况该怎么分析呢?

我们还是要紧紧抓住“递归”这个特性,递归的遍历顺序是“左中右”,所以一定是先遍历根节点再遍历右子树,且父亲的左子树也一定是被遍历完的啦,所以这种情况我们一定是“往回走”。

由上分析可知,如果当前节点是其父亲的右子树的话,那么它的父亲节点也一定被遍历过的了。同理在向上走的过程中,只要遇到当前节点是其父亲的右孩子的情况,其父亲也一定是被遍历过的,只有遇到当前节点是其父亲的左的情况,才说明其父亲是没有被遍历过的。

所以结论是:如果当前节点的右子树为空的话,那么它的下一个节点则是向上寻找到的第一个孩子为父亲左的父亲节点。

 所以我们迭代器前置++的代码也就出来了:

// 前置++
iterator& operator++() {
	assert(_node != nullptr);
	if (_node->_right) { // 如果右不为空,就去寻找右子树的最左节点
		Node* cur = _node->_right;
		while (cur->_left) {
			cur = cur->_left;
		}
		_node = cur;
	}
	else { // 如果为空,就向上寻找第一个孩子为父亲左的父亲节点
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent&& cur = parent->_right) { // 有可能会走到根,所以parent有可能为空
			cur = parent;
			parent = cur->_parent;
		}
		_node = parent;

	}
	return *this;
}

 写完我们可以简单得来测试一下:

3.2、迭代器--的实现

迭代器的--操作我觉得都不用讲了,完全是++反过来就行了,没错!只需要修改一下方向即可。

其实如果要推原理的话也只是和++反方向而已,将遍历方向换成“右中左”像上面++一样的逻辑去推就行了:

// 前置--
iterator& operator--() {
	assert(nullptr != _node);
	if (_node->_left) { // 如果左不为空就去找左子树的最右节点
		Node* cur = _node->_left;
		while (cur->_right) {
			cur = cur->_right;
		}
		_node = cur;	
	}
	else { // 如果左为空,就去向上去找第一个孩子为父亲右的父亲节点
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent && cur == parent->_left) {
			cur = parent;
			parent = cur->_parent;
		}
		_node = parent;
	}
	return *this;
}

为了证明这样是可行的,我还是来测试一下吧:

 

四、const迭代器的实现

const迭代器也是和链表的一样,不需要再复制一份代码,只需要套一层模板即可:

// 红黑树迭代器类
template <class T, class Ref, class Ptr>
class RBTreeIterator {
typedef RBTreeNode<T> Node;
typedef RBTreeIterator<T, Ref, Ptr> iterator;
public :
// 构造
RBTreeIterator(Node* node)
	:_node(node)
{}
// 星号解引用
Ref operator*() {
	return _node->_val;
}
// 箭头解引用
Ptr operator->() {
	return &_node->_val;
}
// 不等于
bool operator!=(const iterator& it) {
	return _node != it._node;
}

// 前置++
iterator& operator++() {
	assert(_node != nullptr);
	if (_node->_right) { // 如果右不为空,就去寻找右子树的最左节点
		Node* cur = _node->_right;
		while (cur->_left) {
			cur = cur->_left;
		}
		_node = cur;
	}
	else { // 如果为空,就向上寻找第一个孩子为父亲左的父亲节点
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent&& cur == parent->_right) { // 有可能会走到根,所以parent有可能为空
			cur = parent;
			parent = cur->_parent;
		}
		_node = parent;
	}
	return *this;
}

 下一步,给红黑树加上const迭代器:

4.1、解决不能修改的问题

其实Set是不分什么const和非const迭代器,因为它里面就只存了一个K,并且是绝对不能修改的,但是为了完整性,我们还是得搞出const和非const迭代器出来。

所以它的const和非const迭代器虽然名字不同,但底层都是const迭代器,写出来就是这样子:

 并且在特原本实现的begin和end后面再加上const修饰即可。

这样我们再想去修改Set中的值就会出错了:

而对于Map,我们是想让它的V可以修改,而K不能修改,所以Map需要分const和非const迭代器,但不管是const或非const迭代器,都不能修改K。

所以我们可以在传递模板参数的时候就解决,K不能被修改的问题,只需要将模板参数中的K修改成const K即可:

并且在迭代器的声明处也需要加上const,不然的话类型是对不上的:

 

还要再加上const和非const的begin和end:

 这样子,我们再想去修改K的时候就会报错:

而V是可以修改的:

但是关于迭代器的问题问题还没有完,因为Map还要实现一个方括号运算符重载,这个重载能做到插入或者修改V,在实现这个重载的时候,还会跳出来一个更加麻烦的问题。

4.2、实现Map的方括号运算符重载

我们先来查查文档,看看Map中的方括号具体的实现形式是怎样的:

 其实看不懂不要就,我们主要是想提取出里面的关键信息,从下方画横线处我们可获悉它的实现是借助inser来实现的。

然后返回值看不懂我们就来看看它对返回值的描述:

所以我们明白了,方括号的返回值就是V的引用。

同时为了实现从insert的返回值中提取出V我们还要修改insert的返回值。

将insert的返回值从bool修改成pair<iterator, bool>

因为上面就符合上面的逻辑了。

而且在所有返回的地方也要进行修改:

所以方括号的实现逻辑就是从insert的返回值中提取出V,反正它插入成功或失败都会返回一个pair:

	// 方括号运算符重载
	V& operator[](const K& key) {
		pair<iterator, bool> ret = insert(make_pair(key, V()));
		return ret.first->second;
	}

但是还没有完成,当我们一编译就会发现一个很严重的问题:

4.3、解决const和非const迭代器的转化问题

为什么会出现上面的这种问题呢?

其实问题就出在我们将insert的返回值修改成pair<iterator, bool>&这里,因为红黑树中insert的返回值中的iterator是一个非const的迭代器:

而在Set中只有const迭代器:

所以在红黑树的insert返回给Set的insert的时候就会产生类型转换,而iterator和const_iterator是两种不同的类型,const_iterator的本质其实是其包含的内容不能修改,并不是iterator本身不能修改。

就比如const int *p和int * const p的区别,前者是指针p指向的内容不能修改,后者是指针本身不能修改。const_iterator就相当于前者,所以iterator和const_iterator是两种不同的类型,并不是权限的放大缩小这么简单。

这个问题 其实有一个很巧妙的方法,我们只将红黑树的insert的返回值由pair<iterator, bool>修改成pair<Node*, bool>就能解决问题了:

纠错!:前面的图中insert给的返回值是引用的形式,其实是不能是引用的,因为有了引用就没拷贝了,这个方法其实就是通过pair的拷贝构造来优化的,用引用的话就失效了。

这个解决方法其实是利用了pair的写得妙的构造函数来实现的:

这其实是一个带模板参数的构造函数,其实这样设计可以做到将构造和拷贝构造合为一体,因为拷贝构造是一定用相同类型的参数来构造的,所以当U和V与要被构造的对象的U和V一样的话,那这就是一个拷贝构造,如果不一样,那就是一个构造。只需要参数里的UV能构造要被构造的对象的UV即可。

而我们这里的pair<Node*, bool>里面的Node*是能够构造iterator的,所以这里使用的就是pair的构造进行转化的。

所以pair<Node*, bool>就能够转化成pair<iterator, bool>了。

不得不说,一切都因为pair的这个构造写得实在太好了。

至此,我们的程序就可以正常运行了:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1275723.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Security 6.x 系列(8)—— 源码分析之配置器SecurityConfigurer接口及其分支实现

一、前言 本章主要内容是关于配置器的接口架构设计&#xff0c;任意找一个配置器一直往上找&#xff0c;就会找到配置器的顶级接口&#xff1a;SecurityConfigurer。 查看SecurityConfigurer接口的实现类情况&#xff1a; 在 AbstractHttpConfigurer 抽象类的下面可以看到所有…

【Unity】模型导入和动画

模型下载和格式转换 在模之屋下载了我推&#xff08;&#xff09; https://www.aplaybox.com/ 获得tex纹理文件和.pmx文件 需要转换为Unity可以使用的.fbx文件 下载Blender2.93和CATS插件 Blender2.93下载页面&#xff1a;https://www.blender.org/download/lts/ CATS插件下…

【Python表白系列】这个情人节送她一个漂浮的爱心吧(完整代码)

文章目录 漂浮的爱心环境需求完整代码详细分析系列文章 漂浮的爱心 环境需求 python3.11.4PyCharm Community Edition 2023.2.5pyinstaller6.2.0&#xff08;可选&#xff0c;这个库用于打包&#xff0c;使程序没有python环境也可以运行&#xff0c;如果想发给好朋友的话需要这…

【VMware相关】VMware vSphere存储方案

一、iSCSI存储 参考文档 VMware官方文档&#xff1a;配置iSCSI适配器和存储 华为配置指南&#xff1a;VMware ESXi下的主机连通性指南 1、配置说明 如下图所示&#xff0c;VMware配置iSCSI存储&#xff0c;需要将物理网卡绑定到VMKernel适配器上&#xff0c;之后再将VMKernel适…

Golang数据类型(数字型)

Go数据类型&#xff08;数字型&#xff09; Go中数字型数据类型大致分为整数&#xff08;integer&#xff09;、浮点数&#xff08;floating point &#xff09;和复数&#xff08;Complex&#xff09;三种 整数重要概念 整数在Go和Python中有较大区别&#xff0c;主要体现在…

2021年11月10日 Go生态洞察:Twelve Years of Go

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Python作用域大揭秘:局部、全局,global关键字

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com Python作用域是编程中关键的概念之一&#xff0c;决定了变量在代码中的可见性和生命周期。本文将深入探讨Python的局部作用域、全局作用域&#xff0c;以及如何使用global关键字来操作全局变量。通过丰富的示例代…

Jmeter测试地图服务性能

一、前言 Jmeter可以用来模拟多用户来访问http&#xff08;s&#xff09;请求&#xff0c;并返回访问结果&#xff0c;而地图服务归根结底仍是个http&#xff08;s&#xff09;请求。所以我们可以使用Jmeter对地图服务进行压力测试。 当然地图服务也有着它的特殊性&#xff0…

AES加密技术:原理与应用

一、引言 随着信息技术的飞速发展&#xff0c;数据安全已成为越来越受到重视的领域。加密技术作为保障数据安全的重要手段&#xff0c;在信息安全领域发挥着举足轻重的作用。AES&#xff08;Advanced Encryption Standard&#xff09;作为一种对称加密算法&#xff0c;自1990年…

算法题--排椅子(贪心)

题目链接 code #include<bits/stdc.h> using namespace std;struct node{int indx;//用来存储数组下标int cnt;//用来计数 };bool cmp(node a,node b){ //判断是否是数字最大的一个就是经过最多谈话人的道return a.cnt>b.cnt; } node row[2010],cow[2010];bool cmp…

C++12.1

三种运算符重载&#xff0c;每个至少实现一个运算符的重载 #include <iostream>using namespace std;class Person {friend const Person operator- (const Person &L, const Person &R);friend bool operator<(const Person &L,const Person &R);f…

TZOJ 1420 手机短号

答案&#xff1a; #include <stdio.h> #include <string.h> int main() {int n 0;scanf("%d", &n);while (n--) //输入n次{char phone[12];scanf("%s", phone);printf("6%s\n", phone 6); //跳过数组前6个元素&#…

数据挖掘实战:基于 Python 的个人信贷违约预测

本次分享我们 Python 觅圈的一个练手实战项目&#xff1a;个人信贷违约预测&#xff0c;此项目对于想要学习信贷风控模型的同学非常有帮助。 技术交流 技术要学会交流、分享&#xff0c;不建议闭门造车。一个人可以走的很快、一堆人可以走的更远。 好的文章离不开粉丝的分享、…

win10 修改任务栏颜色 “开始菜单、任务栏和操作中心” 是灰色无法点击,一共就两步,彻底解决有图有真相。

电脑恢复了一下出厂设置、然后任务栏修改要修改一下颜色&#xff0c;之前会后来忘记了&#xff0c;擦。 查了半天文档没用&#xff0c;最后找到官网才算是看到问题解决办法。 问题现象: 解决办法: 往上滑、找到这里 浅色改成深色、然后就可以了&#xff0c;就这么简单。 w…

美丽的时钟

案例绘制一个时钟 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>美丽的时钟</title><script language"javascript">window.onloadfunction(){var clockdocument.getElementById("clock"…

Ubuntu中MySQL安装与使用

一、安装教程&#xff1a;移步 二、通过sql文件创建表格&#xff1a; 首先进入mysql&#xff1a; mysql -u 用户 -p 回车 然后输入密码source sql文件&#xff08;路径&#xff09;;上面是sql语句哈&#xff0c;所以记得加分号。 sql文件部分截图&#xff1a; 创建成功后的部…

【小布_ORACLE笔记】Part11-1--RMAN Backups

Oracle的数据备份于恢复RMAN Backups 学习第11章需要掌握&#xff1a; 一.RMAN的备份类型 二.使用backup命令创建备份集 三.创建备份文件 四.备份归档日志文件 五.使用RMAN的copy命令创建镜像拷贝 文章目录 Oracle的数据备份于恢复RMAN Backups1.RMAN Backup Concepts&#x…

【无标题】mmocr在云服务器上

这里写目录标题 1、创建虚拟环境2、切换和退出conda虚拟环境3. 显示、复制&#xff08;克隆&#xff09;、删除虚拟环境4、删除环境安装指示中 cd进项目文件夹开始训练模型&#xff08;python XXX.py | tee record.txt 记录训练结果&#xff09;如何在Linux服务器上安装Anacond…

Redis部署-主从模式

目录 单点问题 主从模式 解析主从模式 配置redis主从模式 info replication命令查看复制相关的状态 断开复制关系 安全性 只读 传输延迟 拓扑结构 数据同步psync replicationid offset psync运行流程 全量复制流程 无硬盘模式 部分复制流程 积压缓冲区 实时复…

【代码】基于算术优化算法(AOA)优化参数的随机森林(RF)六分类机器学习预测算法/matlab代码

代码名称&#xff1a;基于算术优化算法&#xff08;AOA&#xff09;优化参数的随机森林&#xff08;RF&#xff09;六分类机器学习预测算法/matlab代码 使用算术优化算法&#xff08;AOA&#xff09;优化分类预测模型的参数&#xff0c;收敛性好&#xff0c;准确率提升明显&am…