用红黑树封装实现map和set

map和set的实现原理

为了方便实现我们的map和set,我们肯定是要养成看源码的习惯的,看了源码之后你才会感受到大佬的强大!

在源码里面,对于map和set的实现,底层是用同一棵红黑树封装出来的,并不是用了两棵红黑树
那么大家肯定会有疑问了,一棵红黑树这么能两用呢,况且map和set的底层存储的节点类型不一样啊,map是存储的键值对,set只是存储key

这时设计map和set的大佬就想到了一个极佳的办法,在红黑树底层中用了一个模板参数Value来代表红黑树结点存储对象的类型,这个类型可能是pair键值对,也有可能是key类型。

红黑树其实并不知道自己的节点是什么类型,只能用模板来接受存储对象的类型

enum Color {RED,BLACK};
template<class T>//节点存储对象类型,可能是键值对也有可能是key
class RBTreeNode
{
	T _data;
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	Color _col;

	RBTreeNode(const T& data)
		:_data(data)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _col(RED)
	{}
};

这样,我们就可以实现一树多用了!

我们就可以根据value的类型来去确定是map还是set了,但是大家可能会有一点小疑问,既然有了value就可以实现红黑树了,那我们还要关键码key有什么用呢?
这里的关键码key是必不可少的!

因为在红黑树搜索的时候,依靠的还是关键码进行搜索,通过所传关键码和红黑树结点中的关键码的大小关系,确定到红黑树中要搜索的某个结点。

红黑树的find函数的参数类型必须是key!

在实现map和set之前,我们先想一想,上一篇博文的红黑树有哪些地方需要进行改进的?

细心的朋友就会发现,上篇博文的代码,插入时比较的就只是在整形情况下的key的比较,但是map和set就只是存储整形吗?显然不是,所以我们就要想办法提取其他类型下的key关键码来进行比较,并且map的节点的value类型是键值对的话,要提取的first,set只需要直接比较key,该如何结局这个问题呢?

我们这个时候就可以使用仿函数了!
我们可以在map和set中各自增加一个仿函数类,在模板传参时,再增加一个仿函数类来提取节点的关键码的参数,我们将他命名为KeyOfT

set

template<class K>
class set
{
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
public:

private:
	RBTree<K, K, SetKeyOfT> _t;
};

map

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

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

rbtree

template<class K,class T,class KeyOfT>
class RBTree
{
	typedef RBTreeNode<T> Node;

public:

private:
	Node* _root = nullptr;
};

这样我们再插入的时候就可以直接提取关键码进行插入了

在这里插入图片描述

map和set的迭代器

关于map和set的迭代器的实现,设计库里面的源码的大佬很巧妙地运用了const

大家都知道,set是不可以修改的,map可以修改,但是修改的也只是pair键值对的value,pair里面的key可是不能修改的

在这里插入图片描述

对红黑树类型中的迭代器类型进行typedef时,可以看到我们在typedef后面加了typename,typename的作用就是告诉编译器后面的东西是一个类型,你先不要编译他,等到模板实例化为真正类型后,你再去取他里面的内嵌类型iterator。因为我们知道编译器是不编译模板的,编译的是模板实例化之后的代码,只有模板实例化之后,它里面的内嵌类型我们才可以取到,所以如果你不加typename,有可能取的不是类型,因为静态变量或函数都是可以通过类+域访问符进行访问的,所以如果你要取模板里面的类型,那就必须在模板前面加typename,告诉编译器你取的是类型。

rbtree迭代器实现

struct __RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __RBTreeIterator<T> Self;
	Node* _node;

	__RBTreeIterator(Node* node)
		:_node(node)
	{}

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

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

	Self& operator++()
	{
		if (_node->_right)
		{
			Node* min = _node->_right;
			while (min->_left)
			{
				min = min->_left;
			}

			_node = min;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = cur->_parent;
				parent = parent->_parent;
			}

			_node = parent;
		}

		return *this;
	}

	Self& operator--()
	{
		//右子树 根 左子树
		if (_node->_left)
		{
			//找左子树的最右结点,其实就是次大的结点
			_node = _node->_left;
			while (_node->_right)
			{
				_node = _node->_right;
			}
		}
		else
		{
			Node* cur = _node;
			Node* parent = _node->_parent;

			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = parent->_parent;
			}
			//如果cur是parent的右,则直接让_node指向到parent即可
			_node = parent;
		}

		return *this;//返回迭代器对象
	}
	bool operator!=(const Self& it)const//const修饰表示在该成员函数内部,不能修改对象的任何成员变量
	{
		return this->_node != it._node;
	}

	bool operator==(const Self& it)const//const对象和非const对象都可以调用
	{
		return this->_node == it._node;
	}
};

那么map和set的实现就很简单了:

template <class K>	
	class set
	{
	public:
		//set表层的普通迭代器底层用的依旧是const_iterator,就是不允许对set的迭代器指向内容进行修改
		typedef typename RBTree<K, K, SetKeyOfT<K>>::const_iterator iterator;
		typedef typename RBTree<K, K, SetKeyOfT<K>>::const_iterator const_iterator;
		iterator begin()  const   //为了防止权限的放大,我们直接使用const,如果对象是const,调用const的begin,不是const也调用const的begin和end
		{
			return _t.begin();
		}
		iterator end()  const
		{
			return _t.end();
		}

map的迭代器是需要实现两个版本的,因为map容器中的数据是可以被修改的,如果你想修改map中的数据那就用iterator,如果你想修改map中的数据那就用const_iterator。
如果是const_iterator,那解引用或者→返回的就是键值对的常引用或const修饰的指向键值对的结构体指针,那么此时键值对的key和value都是不可以修改的。
如果是iterator,解引用或者→返回的就是键值对的普通引用或无const修饰的指向键值对的结构体指针,但此时键值对的key依旧不可以被修改,只能对键值对中的value进行修改所以即使你用的是iterator,他所指向的键值对的key依旧是不能修改的,我们只能修改他的value。

template <class K, class V>
class map
{
public:

	typedef typename RBTree<K, pair<const K, V>, MapKeyOfT<K, V>>::const_iterator const_iterator;
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT<K, V>>::const_iterator const_iterator;

	//map的迭代器需要提供两个版本
	iterator begin()
	{
		return _t.begin();
	}
	iterator end()
	{
		return _t.end();
	}
	const_iterator begin()const
	{
		return _t.begin();
	}
	const_iterator end()const
	{
		return _t.end();
	}
private:
	RBTree<K, pair<const K, V>, MapKeyOfT<K,V>> _t;
};

红黑树插入的改造

我们使得插入后其返回值为键值对,如果出现插入key值相等的情况,则将bool改为false即可。最后调色后返回键值对时,我们不能直接返回cur构造的迭代器,因为如果发生调色,则cur位置会更改,所以在插入结点后,用一个curit的结点指针记录一下插入结点的位置,最后返回由curit结点指针构造出来的迭代器

pair<iterator, bool> Insert(const T& data)
//红黑树必须通过结点里面存储的key来进行比较才可以插入结点,所以出现了kot仿函数对象
{
	KeyOfT kot;
	Node* parent = nullptr;
	Node* cur = _root;
	if (_root == nullptr)
	{
		_root = new Node(data);
		_root->_col = BLACK;
		return make_pair(iterator(_root), true);
	}

	while (cur)
	{
		if (kot(cur->_data) < kot(data)) 
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (kot(cur->_data) > kot(data))
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return make_pair(iterator(cur), false);
		}
	}
	cur = new Node(data);
	Node* curit = cur;//保存一下cur的位置否则下面旋转调色过程中,cur有可能被改了
	if (kot(parent->_data) > kot(data))
	{
		parent->_left = cur;
		cur->_parent = parent;
	}
	else
	{
		parent->_right = cur;
		cur->_parent = parent;
	}

	while (parent && parent->_col == RED)
	{
		Node* grandparent = parent->_parent;
		if (parent == grandparent->_left)
		{
			Node* uncle = grandparent->_right;

			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandparent->_col = RED;

				cur = grandparent;
				parent = cur->_parent;
			}
			else if (cur == parent->_left)
			{
				RotateR(grandparent);
				grandparent->_col = RED;
				parent->_col = BLACK;
				break;
			}
			else if (cur == parent->_right)
			{
				RotateL(parent);
				RotateR(grandparent);
				cur->_col = BLACK;
				grandparent->_col = RED;
				break;
			}
			else
			{
				assert(false);
			}
		}
		else
		{
			Node* uncle = grandparent->_left;
			if (uncle && uncle->_col == RED)
			{
				grandparent->_col = RED;
				uncle->_col = parent->_col = BLACK;

				cur = grandparent;
				parent = cur->_parent;
			}
			else if (cur == parent->_right)
			{
				RotateL(grandparent);
				parent->_col = BLACK;
				grandparent->_col = RED;
				break;
			}

			else if (cur == parent->_left)
			{
				RotateR(parent);
				RotateL(grandparent);
				cur->_col = BLACK;
				grandparent->_col = RED;
				break;
			}
			else
			{
				assert(false);
			}
		}
	}
	_root->_col = BLACK;
	
	return make_pair(iterator(curit), true);
}

map的[]重载

[]直接返回插入节点的值,如果[]内的key值不在红黑树中,就直接插入,若在其中也有进行修改,最后返回iterator的first的second

template <class K, class V>
class map
{
public:
	pair<iterator, bool> insert(const pair<const K, V>& kv)
	{
		return _t.Insert(kv);
	}
	
	V& operator[](const K& key)
	{
		pair<iterator, bool> ret = insert(make_pair(key, V()));

		return (ret.first)->second;
	}
private:
	RBTree<K, pair<const K, V>, MapKeyOfT<K,V>> _t;
};

由于博主的能力有限,部分讲解不到位,等我巩固知识后再做修正!希望大家指正!本篇博文的分享到这里就结束了,感谢大家的支持!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring基础——Bean定义的继承(Bean配置中的parent属性)

Bean的继承 当一个Bean中定义了很多配置信息&#xff0c;可以将一部分固定信息抽象成父Bean&#xff0c;子Bean从父Bean继承配置数据&#xff0c;并根据需要可以覆盖或添加其他数据&#xff0c;这样可以使开发变的更加高效。 父Bean的定义 父Bean所定义的配置属性子Bean必须…

JWT的是什么

session共享 什么是session共享 Session共享是指在分布式系统中&#xff0c;在多个服务器之间共享同一个用户的会话数据。在传统的Web应用中&#xff0c;用户的会话信息通常存储在服务器端的Session中&#xff0c;而每个用户的请求在同一个服务器上处理&#xff0c;因此可以轻…

智奇科技工业 Linux 屏更新开机logo

智奇科技工业 Linux 屏更新开机logo 简介制作logo.img文件1、转换格式得到logo.bmp2、使用Linux命令生成img文件 制作rootfs.img文件替换rootfs.img中的logo 生成update.img固件附件 简介 智奇科技的 Linux 屏刷开机logo必须刷img镜像文件&#xff0c;比较复杂。 制作logo.i…

深度学习-Pytorch实现经典AlexNet网络:山高我为峰

深度学习-Pytorch实现经典AlexNet网络之山高我为峰 深度学习中&#xff0c;经典网络引领一波又一波的技术革命&#xff0c;从LetNet到当前最火的GPT所用的Transformer&#xff0c;它们把AI技术不断推向高潮。2012年AlexNet大放异彩&#xff0c;它把深度学习技术引领第一个高峰…

AI论文速读 | 大语言模型作为城市居民——利用LLM智能体框架生成人类移动轨迹

题目&#xff1a;Large Language Models as Urban Residents: An LLM Agent Framework for Personal Mobility Generation 作者&#xff1a;Jiawei Wang (王家伟), Renhe Jiang&#xff08;姜仁河&#xff09;, Chuang Yang&#xff08;杨闯&#xff09;, Zengqing Wu&#xf…

JeeSite Vue3:前端开发的未来之路

JeeSite Vue3&#xff1a;前端开发的未来之路 随着技术的飞速发展&#xff0c;前端开发技术日新月异。在这个背景下&#xff0c;JeeSite Vue3 作为一个基于 Vue3、Vite、Ant-Design-Vue、TypeScript 和 Vue Vben Admin 的前端框架&#xff0c;引起了广泛关注。它凭借其先进的技…

mapbox加载全球3D建筑

本案例使用Mapbox GL JavaScript库进行加载全球3D建筑。 文章目录 1. 引入 CDN 链接2. 创建地图3. 监听地图加载完成事件3.1. 获取地图的样式中的图层3.2. 查找图层3.3. 添加三维建筑图层 4. 演示效果5. 代码实现 1. 引入 CDN 链接 <!-- 1.引入CDN链接 --> <script sr…

C++模拟揭秘刘谦魔术,领略数学的魅力

新的一年又开始了&#xff0c;大家新年好呀~。在这我想问大家一个问题&#xff0c;有没有同学看了联欢晚会上刘谦的魔术呢&#xff1f; 这个节目还挺有意思的&#xff0c;它最出彩的不是魔术本身&#xff0c;而是小尼老师“念错咒语”而导致他手里的排没有拼在一起&#xff0c;…

Android studio 侧边栏看不到 Commit 标签,不能方便的查看本地ChangaeList

参考 如上图&#xff0c;一次升级后找不到commit 标签&#xff0c;造成不能很好的监测本地修改了那些文件&#xff0c;通过搜索找到显示的方法。&#xff0c;进入设置找红框位置&#xff0c;勾选复选款即可。 正常显示

Python实现CCI工具判断信号:股票技术分析的工具系列(5)

Python实现CCI工具判断信号&#xff1a;股票技术分析的工具系列&#xff08;5&#xff09; 介绍算法解释 代码rolling函数介绍完整代码data代码CCI.py 介绍 在股票技术分析中&#xff0c;CCI (商品路径指标&#xff09;是一种常用的技术指标&#xff0c;用于衡量股价是否处于超…

JavaWeb Request:获取请求数据

Request是请求对象&#xff0c;Response是响应对象。 浏览器会发送HTTP请求到后台服务器[Tomcat]&#xff0c;请求中会包含很多请求数据 [请求行请求头请求体] &#xff0c;后台服务器[Tomcat]会对HTTP请求中的数据进行解析并把解析结果存入到Request对象&#xff0c;可以从Req…

Docker之数据卷

目录 一、什么是数据卷 二、自定义镜像 一、什么是数据卷 1.1Docker 数据管理 在生产环境中使用 Docker &#xff0c;往往需要对数据进行持久化&#xff0c;或者需要在多个容器之间进行 数据共享&#xff0c;这必然涉及容器的数据管理操作 二、操作 将宿主机的目录与容器的…

双通道音频功率放大电路,外接元件少, 通道分离性好,3V 的低压下可正常使用——D2025

D2025 为立体声音频功率放大集成电路&#xff0c;适用于各类袖珍或便携式立体声 收录机中作功率放放大器。 D2025 采用 DIP16 封装形式。 主要特点&#xff1a;  适用于立体声或 BTL 工作模式  外接元件少  通道分离性好  电源电压范围宽&#xff08;3V~12V &#xff…

深度学习GPU环境安装(WINDOWS安装NVIDIA)

1.检测是否支持GPU环境 1.1.打开设备管理器 winows下面搜索设备管理器&#xff08;或者从桌面"此电脑"——>右键点击——>"管理"打开&#xff09; 1.2.查看本地显卡 在"设备管理器"——"显示适配器"中&#xff0c;如果没有&…

瑞吉外卖项目详细分析笔记及所有功能补充代码

目录 项目刨析简介技术栈项目介绍项目源码 一.架构搭建1.初始化项目结构2.数据库表结构设计3.项目基本配置信息添加公共字段的自动填充全局异常处理类返回结果封装的实体类 二.管理端业务开发1.员工管理相关业务1.1员工登录1.2员工退出1.3过滤器拦截1.4员工信息修改1.5员工信息…

ElasticSearch之分片相关概念segment,merge,refresh等

写在前面 本文看下分片相关概念&#xff0c;segment&#xff0c;merge&#xff0c;refresh等。 1&#xff1a;segment&#xff0c;commit point&#xff0c;.del 一个倒排索引的文件称为segment&#xff0c;多个segment组合在一起就是lucene的index&#xff0c;也就是es的sh…

线程变量ThreadLocal用于解决多线程并发时访问共享变量的问题。

ThreadLocal介绍 ThreadLocal叫做线程变量&#xff0c;用于解决多线程并发时访问共享变量的问题。意思是ThreadLocal中填充的变量属于当前线程&#xff0c;该变量对其他线程而言是隔离的&#xff0c;也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建…

12. Nginx进阶-Location

简介 Nginx的三大区块 在Nginx中主要配置包括三个区块&#xff0c;结构如下&#xff1a; http { #协议级别include /etc/nginx/mime.types;default_type application/octet-stream;log_format main $remote_addr - $remote_user [$time_local] "$r…

ssl证书无效是否继续访问啥意思

SSL证书&#xff08;Secure Sockets Layer&#xff09;是现代互联网通信安全的基础组成部分&#xff0c;尤其对于涉及敏感信息交换的HTTPS站点至关重要。当浏览器提示“SSL证书无效”时&#xff0c;这意味着浏览器无法验证网站的身份或确定其加密连接的安全性。这种情况下&…