C++——用红黑树封装map和set

news2024/11/15 14:03:18

目录

1. 前言

2. 红黑树模板参数的控制

3. 模板参数中仿函数的增加

4. 红黑树迭代器的实现

5. 红黑树的begin()和end()

6. 红黑树的Find查找函数

7. 红黑树封装map和set源码

7.1 map.h

7.2 set.h

7.3 test.cpp

1. 前言

我们都知道set是K模型的容器,而map是KV模型的容器,但是它俩的底层都是用红黑树实现的,上篇博文中模拟实现了一颗红黑树,接下来将对其进行改造,继而用一颗红黑树完美的封装map和set。说白了map和set就是一个光杆司令,其内部的主要功能都是套用了红黑树现成的,只是稍作改动即可。

2. 红黑树模板参数的控制

 既然set是K模型,map是KV模型,正如stl库里的map和set,如图所示:

 前面实现的红黑树是KV模型,那么为了适配set还需要重新写一个红黑树吗?实际上大可不必,只需要对模板参数进行修改即可,下面为库中红黑树正对模板参数进行修改解决的:

 通过这里就能够很清晰的看出库里的节点的存储类型是根据set和map的第二个模板参数决定的,第二个参数存的是什么,节点存的就是什么类型,继而可以满足set和map的需求,而现在又可能引发一个新的问题:

  • 既然数据类型看第二个模板参数,那第一个模板参数有何用处? 

因为在红黑树中,无可避免会要求实现find等对K有需求的函数,因为find函数主要是通过Key进行查找的,如若省略第一个模板参数,那么map就无法进行find查找操作。

接下来,我们按照库里红黑树的样子对我们自己写的进行一个调整:

//节点类
template <class T>
struct RBTreeNode
{
	//三叉链结构
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
    //存储的数据
	T _data;
	//节点的颜色
	Colour _col;
	//构造函数
	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(Red)
	{}
};
// 红黑树的类
// T决定红黑树中存储的什么数据
// set RBTree<K, K>
// map RBTree<K, pair<K, V>>
template <class K, class T>
class RBTree
{
	typedef RBTreeNode<T> Node;//T决定节点存储类型的数据
public:
    //……
private:
	Node* _root = nullptr;
};

对红黑树的模板参数修改好了,那么map(KV模型)和set(K模型)自然而然就能够适配了:

  • set:
namespace cpp
{
	template<class K>
	class set
	{
    public:
        //……
	private:
		RBTree<K, K> _t;
	};
}
  • map:
namespace cpp
{
	template<class K, class V>
	class map
	{
    public:
        //……
	private:
		RBTree<K, pair<K, V>> _t;
	};
}

3. 模板参数中仿函数的增加

由于现在红黑树的节点类型是T,当容器为set时,T就是键值Key,可以直接进行比较,当容器是map时,T就是pair<Key, Value>,此时不能直接比较,而需要从此键值对中取出Key,再拿Key进行大小比较。

为了解决这一点,我们可以在红黑树的模板参数上再加一层仿函数,此参数专门用于获得Key类型,而这个仿函数的实现是在map和set内部封装的,具体操作如下:

  • map:
template<class K, class V>
class map
{
    //仿函数
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;//返回键值key
		}
	};
public:
    //……
private:
	RBTree<K, pair<K, V>, MapKeyOfT> _t;
};
  • set:
template<class K>
class set
{
    //仿函数
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
public:
    //……
private:
	RBTree<K, K, SetKeyOfT> _t;
};

下面画图演示具体的调用情况:

4. 红黑树迭代器的实现

红黑树的正向迭代器实际上就是对结点指针进行了封装,因此在正向迭代器当中实际上就只有一个成员变量,那就是正向迭代器所封装结点的指针。

//迭代器的类
template<class T, class Ref, class Ptr>
struct __RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __RBTreeIterator<T, Ref, Ptr> Self;//把迭代器typedef,简化后续使用
    //……
	Node* _node;
};

而在其内部,我们要完成如下操作:

  • 1、构造函数
  • 2、*和->运算符重载
  • 3、!=和==运算符重载
  • 4、++运算符重载
  • 5、--运算符重载

接下来具体展开演示:

  • 1、构造函数

构造函数我们直接通过一个节点的指针从而构造一个正向迭代器即可。

//构造函数
__RBTreeIterator(Node* node)
	:_node(node)
{}
  • 2、*和->运算符重载

*运算符就是解引用,直接返回对应节点数据的引用即可。而->运算符返回的是对应节点数据的指针。

//*运算符重载
Ref operator*()
{
	return _node->_data;//返回_data数据本身
}
//->运算符重载
Ptr operator->()
{
	return &_node->_data;//返回_data数据的地址
}
  • 3、!=和==运算符重载

!=运算符直接返回两个节点是否不同,而==运算符直接返回两个节点是否相同即可。

//!=
bool operator!=(const Self& s)
{
	return _node != s._node;
}
//==
bool operator==(const Self& s)
{
	return _node == s._node;
}
  • 4、++运算符重载

++运算符又分前置++和后置++。

  • 前置++:

首先,这里红黑树迭代器里的++后的值应该是按此位置开始往后中序遍历的下一个。而这个下一个节点的值理应比原先的大,想要找到这个位置,结合二叉搜索树的性质,理应在右子树当中去寻找,而这又要看右子树是否为空,具体操作如下:

  • 1、右子树非空:直接遍历找到右子树的最左节点即可;
  • 2、右子树为空:找祖先里面,孩子是父亲左的那个祖先节点;
  • 3、当parent遍历到空时,++结束;
  • 4、注意前置++返回的是++后的值。
//前置++
Self& operator++()
{
	if (_node->_right == nullptr)//右子树为空
	{
		//找祖先里面,孩子是父亲左的那个
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent && parent->_right == cur)
		{
			cur = cur->_parent;
			parent = parent->_parent;
		}
		_node = parent;
	}
	else//右子树不为空
	{
		//右子树的最左节点
		Node* subLeft = _node->_right;
		while (subLeft->_left)
		{
			subLeft = subLeft->_left;
		}
		_node = subLeft;
	}
	return *this;
}
  • 后置++:

后置++和前置++的唯一区别就在于后置++是返回++前的值,这里只需要在前置++的基础上在一开始把当前节点保存起来,直接调用前置++,最后返回保存的那个节点值即可。

//后置++
Self& operator++(int)
{
	Self tmp(*this);
	++(*this);//复用前置++
	return tmp;
}
  • 5、--运算符重载

--运算符又分为前置--和后置--,下面分别讨论:

  • 前置--:

--运算符和++运算符相反,--运算符是找比当前位置次小的节点而这个节点,而这又要优先去左子树里寻找,而左子树又分为如下两种情况:

  • 左子树为空,找祖先里面孩子是父亲右的那个节点;
  • 左子树非空,找左子树里最右的节点。
//前置--
Self& operator--()
{
	if (_node->_left == nullptr)//左子树为空
	{
		//找孩子是父亲右的那个
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent && parent->_left == cur)
		{
			cur = cur->_parent;
			parent = parent->_parent;
		}
		_node = parent;
	}
	else//左子树不为空
	{
		//找左子树的最右节点
		Node* subRight = _node->_left;
		while (subRight->_right)
		{
			subRight = subRight->_right;
		}
		_node = subRight;
	}
	return *this;
}
  • 后置--:

注意后置--返回的是--前的值,所以先定义tmp把*this保存起来,再套用前置--函数进行自减,最后返回tmp。

//后置--
Self& operator--(int)
{
	Self tmp(*this);
	--(*this);
	return tmp;
}

5. 红黑树的begin()和end()

迭代器实现后,我们需要在红黑树的实现当中进行迭代器类型的typedef。需要注意的是,为了让外部能够使用typedef后的正向迭代器类型iterator,我们需要在红黑树的public区域进行typedef。

template <class K, class T, class KeyOfT>
class RBTree
{
    //……
public:
	typedef __RBTreeIterator<T, T&, T*> iterator;//普通迭代器
	typedef __RBTreeIterator<T, const T&, const T*> const_iterator;//const迭代器
    //……
}

其实,STL中的红黑树的底层是有一个哨兵位头结点的,如下所示:

STL明确规定,begin()与end()代表的是一段前闭后开的区间,而对红黑树进行中序遍历后,可以得到一个有序的序列,因此:begin()可以放在红黑树中最小节点(即最左侧节点)的位置,end()放在最大节点(最右侧节点)的下一个位置,关键是最大节点的下一个位置在哪块?能否给成nullptr呢?答案是行不通的,因为对end()位置的迭代器进行--操作,必须要能找最后一个元素,此处就不行,因此最好的方式是将end()放在头结点的位置

虽说库里的红黑树实现的是带哨兵位头节点的,但毕竟咱这是模拟(但求大概),综上begin()和end()的指向如下总结:

  • begin():指向红黑树中最小节点(即最左侧节点)的位置.
  • end():指向红黑树中最大节点(最右侧节点)的下一个位置,即nullptr.
//begin()
iterator Begin()//找最左节点
{
	Node* subLeft = _root;
	while (subLeft && subLeft->_left)
	{
		subLeft = subLeft->_left;
	}
	return iterator(subLeft);//调用迭代器的构造函数
}
//end()
iterator End()//返回最右节点的下一个
{
	return iterator(nullptr);
}

当然这里最好再实现一个const版本的begin()和end(),为的是普通迭代器和const迭代器都能够使用,其实主要还是set的迭代器不能被修改,无论是普通迭代器还是const迭代器,其内部都是用const迭代器封装的,因此必须实现一个const版本的begin()和end()。

//const版本
//begin() 
const_iterator Begin() const//找最左节点
{
	Node* subLeft = _root;
	while (subLeft && subLeft->_left)
	{
		subLeft = subLeft->_left;
	}
	return const_iterator(subLeft);//调用迭代器的构造函数
}
//end()
const_iterator End() const//返回最右节点的下一个
{
	return const_iterator(nullptr);
}

6. 红黑树的Find查找函数

 查找的规则很简单,只需要遍历节点即可,具体规则如下:

  1. 如果查询的值 > 当前节点值,遍历到右子树查询
  2. 如果查询的值 < 当前节点值,遍历到左子树查询
  3. 如果查询的值 = 当前节点值,返回当前位置的迭代器
  4. 如果循环结束,说明未查询到,返回End()
//Find查找函数
iterator 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 iterator(cur);
		}
	}
	return End();
}

7. 红黑树封装map和set源码

7.1 map.h

#pragma once
#include"RBTree.h"
namespace cpp
{
	template<class K, class V>
	class map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator;
		typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::const_iterator const_iterator;
		//begin()
		iterator begin() 
		{
			return _t.Begin();
		}
		//end()
		iterator end() 
		{
			return _t.End();
		}
		//insert
		pair<iterator, bool> insert(const pair<K, V>& kv)
		{
			return _t.Insert(kv);
		}
		//Find
		iterator find(const K& key)
		{
			return _t.Find();
		}
		//operator[]
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			return ret.first->second;
		}
	private:
		RBTree<K, pair<K, V>, MapKeyOfT> _t;
	};
}

7.2 set.h

#pragma once
#include"RBTree.h"
namespace cpp
{
	template<class K>
	class set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;
		typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;
		//begin()
		iterator begin() const
		{
			return _t.Begin();
		}
		//end()
		iterator end() const
		{
			return _t.End();
		}
		//insert
		pair<iterator, bool> insert(const K& key)
		{
			//pair<typename RBTree<K, K, SetKeyOfT>::iterator, bool> ret = _t.Insert(key);
			auto ret = _t.Insert(key);
			return pair<iterator, bool>(iterator(ret.first._node), ret.second);
		}
		//Find
		iterator find(const K& key)
		{
			return _t.Find();
		}
	private:
		RBTree<K, K, SetKeyOfT> _t;
	};
}

7.3 test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include<string>
#include"map.h"
#include"set.h"
void test_set1()
{
	cpp::set<int> s;
	s.insert(8);
	s.insert(6);
	s.insert(11);
	s.insert(5);
	s.insert(6);
	s.insert(7);
	s.insert(10);
	s.insert(13);
	s.insert(12);
	s.insert(15);
	cpp::set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
}
void test_map1()
{
	cpp::map<string, int> m;
	m.insert(make_pair("111", 1));
	m.insert(make_pair("555", 5));
	m.insert(make_pair("333", 3));
	m.insert(make_pair("222", 2));
	cpp::map<string, int>::iterator it = m.begin();
	while (it != m.end())
	{
		cout << it->first << ":" << it->second << endl;
		it++;
	}
	cout << endl;
	for (auto& kv : m)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	cout << endl;
}
void test_map2()
{
	string arr[] = { "ƻ", "", "ƻ", "", "ƻ", "ƻ", "", "ƻ", "㽶", "ƻ", "㽶" };
	cpp::map<string, int> countMap;
	for (auto& str : arr)
	{
		countMap[str]++;
	}

	for (const auto& kv : countMap)
		cout << kv.first << ":" << kv.second << endl;
}
void test_map3()
{
	cpp::map<string, string> dict;
	dict["insert"];
	dict["insert"] = "";
	dict["left"] = "";
}
int main()
{
	//test_set1();
	//test_map1();
	//test_map2();
	test_map3();
	return 0;
}

 

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

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

相关文章

【深度学习】6-3 卷积神经网络 - 卷积层和池化层的实现

卷积层和池化层的实现 如前所述&#xff0c;CNN 中各层间传递的数据是 4 维数据。所谓 4 维数据&#xff0c;比如数据的形状是 (10, 1, 28, 28)&#xff0c;则它对应 10 个高为 28、长为 28、通道为 1 的数据。用 Python 来实现的话&#xff0c;如下所示 >>> x np.r…

CentOS 7.9 安装 Docker

CentOS 7.9 安装 Docker 文章目录 CentOS 7.9 安装 Docker一、相关博客二、安装 Docker1、安装 device-mapper-persistent-data 和 lvm2 两个依赖2、添加阿里云 Docker 镜像源3、安装 Docker 社区版4、启动5、运行测试 三、配置阿里云镜像加速器 一、相关博客 【Docker】002-D…

基于ASP.NET MVC的网络书店系统/书店商城

摘 要 随着书店规模的不断扩大&#xff0c;人流数量的急剧增加&#xff0c;有关书店的各种信息量也在不断成倍增长。面对庞大的信息量&#xff0c;就需要有网络书店来提高书店工作的效率。通过这样的系统&#xff0c;我们可以做到信息的规范管理和快速查询&#xff0c;从而减少…

1Panel 安装部署

1Panel 是一个现代化、开源的 Linux 服务器运维管理面板。 1. 环境要求 安装前请确保您的系统符合安装条件&#xff1a; 操作系统&#xff1a;支持主流 Linux 发行版本&#xff08;基于 Debian / RedHat&#xff0c;包括国产操作系统&#xff09;&#xff1b; 服务器架构&#…

LangChain: LLM应用开发框架

GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡⚡ Building applications with LLMs through composability ⚡ - GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡https://github.c…

6.22 驱动开发作业

字符设备驱动内部实现原理 1.字面理解解析&#xff1a; 字符设备驱动的内部实现有两种情况&#xff1a; 情况1.应用层调用open函数的内部实现&#xff1a; open函数的第一个参数是要打开的文件的路径&#xff0c;根据这个路径 虚拟文件系统层VFS 可以找到这个文件在文件系统…

Cookie,Session,Token,JWT授权方式对比

文章目录 HTTPCookieSessionSession认证流程Session 共享方案 TokenToken认证流程 JWTJWT认证流程 HTTP HTTP 本质上是无状态的&#xff0c;每个请求都是互相独立、毫无关联的&#xff0c;协议不要求客户端或服务器记录请求相关的信息。服务端无法确认当前访问者的身份信息&…

升级一下《单词猜谜》

网上的单词猜谜都是英文的&#xff0c;不会英语的头痛 于是&#xff0c;我把《单词猜谜》改成中文&#xff0c;并且加上了点新功能&#xff1a; import random as rdmc rdm.randint(1, 3)name1 input("你的名字叫什么&#xff1f;&#xff1a;") if c 1:turns1 …

详解BigDecimal

目录 1.概述 2.基本API 2.1.创建 BigDecimal 对象&#xff1a; 2.3.基本运算方法&#xff1a; 2.4.精度控制方法&#xff1a; 2.5.比较 2.6.转换 3.注意事项 4.底层实现原理 1.概述 精度丢失&#xff0c;由于现代计算机中采用了浮点数来表示小数&#xff0c;这种表示…

小马哥训练营-Java EE单体架构

什么是Servlet Servlet 是一种基于 Java 技术的 Web 组件&#xff0c;用于生成动态内容&#xff0c;由容器管理。类似于其他 Java 技术组件&#xff0c;Servlet 是平台无关的 Java 类组成&#xff0c;并且由 Java Web 服务器加载执行。通常情况&#xff0c;由 Servlet 容器提供…

(十一)CSharp-LINQ-查询表达式(2)

一、查询表达式 1、查询表达式的结构 查询表达式由 from 子句和查询主体组成。 子句必须按照一定的顺序出现。from 子句和 select…group 子句这两部分是必需的。其他子句是可选的。在 LINQ 查询表达式中&#xff0c;select 子句在表达式最后。可以有任意多的 from…let…wh…

Nginx服务器的六个修改小实验

一、Nginx虚拟主机配置 1.基于域名 &#xff08;1&#xff09;为虚拟主机提供域名解析 配置DNS 修改/etc/hosts文件 &#xff08;2&#xff09;为虚拟主机准备网页文档 #创建网页目录 mkdir -p /var/www/html/abc mkdir -p /var/www/html/def ​ #编写简易首页html文件 ec…

Finalshell安全吗?Xshell怎么样?

文章目录 一、我的常用ssh连接工具二、Xshell2.1 下载&#xff1a;认准官网2.2 Xshell 配置2.3 Xftp和WinSCP 一、我的常用ssh连接工具 之前讲过&#xff1a; 【服务器】远程连接选SSH&#xff08;PUTTY、Finalshell、WinSCP&#xff09; 还是 远程桌面&#xff08;RDP、VNC、…

代码随想录训练营第四十二天|01背包、416.分割等和子集

01背包 代码随想录理论讲解 背包问题分类 01背包 问题描述 有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i]。每件物品只能用一次&#xff0c;求解将哪些物品装入背包里物品价值总和最大。 动归五部曲 确定dp数组及下…

基于java web高校社交系统 /springboot高校社交系统

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的系统应运而生&#xff0c;各行各业相继进入信息管理时代&#…

CSS处理页面元素浮动的几个办法

CSS处理页面元素浮动的几个办法 不使用浮动的情况下&#xff0c;子盒子1和2分别在盒子1中占据一行的空间。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" cont…

Java 8新特性:方法引用的介绍与使用

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 1. 什么是方法引用&#xff1f; 2. 方法引用的…

【30天熟悉Go语言】9 Go函数全方位解析

作者&#xff1a;秃秃爱健身&#xff0c;多平台博客专家&#xff0c;某大厂后端开发&#xff0c;个人IP起于源码分析文章 &#x1f60b;。 源码系列专栏&#xff1a;Spring MVC源码系列、Spring Boot源码系列、SpringCloud源码系列&#xff08;含&#xff1a;Ribbon、Feign&…

【好书精读】网络是怎样连接的 之 数据收发完成之后 从服务器断开并删除套接字

&#xff08; 该图由AI制作 &#xff09; 目录 数据收发完成后协议栈要执行的操作 数据发送完毕后断开连接 删除套接字 数据收发操作小结 第一步是创建套接字 然后 客户端会向服务器发起连接操作 数据收发阶段 执行断开操作 数据收发完成后协议栈要执行的操作 数据发…

html_css模拟端午赛龙舟运动

文章目录 ⭐前言&#x1f496; 样式布局&#x1f496; 添加龙舟&#x1f496; 添加css_animation运动 ⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本期给大家分享css实现赛龙舟运动。 &#x1f496; 样式布局 风格&#xff1a;卡通 首先采用一张包括水元素的照片…