【C++】mapset利用红黑树进行简单封装

news2024/11/19 14:33:36

前言

        大家好~~~~呀!很荣幸你能点击这篇文章。本篇也是我的一份学习笔记,让我们一起共同成长吧~ing......

C++红黑树的简单插入实现博客~

【C++】红黑树的插入实现_柒海啦的博客-CSDN博客

二叉搜索树的基本结构和实现博客~

【C++】二叉搜索树_柒海啦的博客-CSDN博客_c++二叉搜索树

        本篇笔记基于红黑树的基本插入实现,实现简单的map和set的包装,掌握底层的基本原理即可~

 (嘿嘿玉子阔爱~~~)

目录

前言

一、map和set的基本使用

1.set的使用

1.1set的模板参数:

1.2set的迭代器:

1.3set的修改:

1.4set的操作:

1.5代码测试:

2.map的使用

2.1map的模板参数:

2.2map的迭代器:

2.3map的修改:

2.4map的操作: 

2.5代码操作:

二、基于插入实现的红黑树进行封装

1.优化红黑树的模板参数

上层代码初步实现:

下层红黑树代码:

 2.红黑树的迭代器实现

重载++/--运算符:

3.map、set上层对红黑树迭代器的相关封装:

4.mapset综合简单封装代码:


一、map和set的基本使用

        在一开始,我们接触的C++STL容器比如:vector、list、stack、deque....这些容器的底层都是为线性序列的数据结构,存储的就是元素本身。这些就是序列式容器

        除了序列式容器外,还有关联式容器。它也是存储数据的,只不过和序列式容器不同的是内部存储的是key-value结构的键值对,并且数据结构也是非线性的(二叉树)。而现在我们要介绍的map和set均为关联式容器。

1.set的使用

set文档:set - C++ Reference (cplusplus.com)

multiset文档:multiset - C++ Reference (cplusplus.com)

1.set是按照一定顺序进行存储的容器。

2.set/multiset 底层数据均是<key, key>的键值对。插入只需要一个key类型即可。

3.set的元素不可重复,multiset的元素可重复。

4.使用set的迭代器进行遍历可以得到有序序列。

5.set的元素默认按照小于来比较,查找某个元素时间复杂度为log_2 N,并且元素不可修改

6.底层使用二叉搜索树-红黑树进行实现。

(注:下述以set为准,multiset使用接口类似)

1.1set的模板参数:

T -  插入元素的数据类型。

Compare - 比较仿函数。默认给的less为库中实现的小于比较。

Alloc - 内存分配

(上述模板在模拟实现中目前只关注T)

1.2set的迭代器:

(解引用返回的就是T这个元素类型) 

1.3set的修改:

insert -  插入T类型的元素,或者在一段迭代器区间进行插入(模拟实现只实现第一种) erase - 删除对应迭代器指向元素,或者一段迭代器区间的元素(模拟实现只实现第一种)

1.4set的操作:

find - 根据传入的元素返回一个迭代器。没有找到返回的就是end()。

count - 根据传入的元素返回其出现的个数(对于multiset有效)

1.5代码测试:

void test2()
{
	//set的使用 - 底层使用的是红黑树进行封装的
	set<int> s;
	int arr[] = { 4, 2, 1, 1, 4, 3, 9, 101, 87, -3 };
	// 特性:去重,迭代器访问是有序的
	for (auto e : arr) s.insert(e);
	set<int>::iterator si = s.begin();
	while (si != s.end())
	{
		cout << *si << " ";
		//*si = 2;  set不允许修改值
		si++;
	}
	cout << endl;
	set<int, greater<int>> s2;  // 第二个是配置的仿函数 - 给其底层对应key的比较大小
	for (auto e : arr) s2.insert(e);
	si = s2.begin();
	while (si != s2.end())
	{
		cout << *si << " ";  // 101 87 9 4 3 2 1 -3
		si++;
	}
	cout << endl;
	// 可删除
	s2.erase(-3);
	s2.erase(s2.begin());
	si = s2.begin();
	while (si != s2.end())
	{
		cout << *si << " ";  // 87 9 4 3 2 1
		si++;
	}
	cout << endl;
	cout << s2.count(4) << endl;  // 返回对应个数 -- 对于不可冗余的set没啥用
	// 查找,返回迭代器 set具有const,无法修改
	cout << *s2.find(87) << endl;
	//cout << *s2.find(10) << endl;  // 返回的是end
	multiset<int> ms(arr, arr + (sizeof(arr) / sizeof(arr[0])));  // 可以使用迭代器构造 - 使用的是multi 可以重复的使用元素哦~
	cout << ms.count(4) << endl;  // 2
}

运行结果: 

2.map的使用

map文档:map - C++ Reference (cplusplus.com)

multimap文档:multimap - C++ Reference (cplusplus.com)

1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素(pair)。

2.同样的默认小于进行比较,比较的元素也key类型的元素。查找类型同样是key,时间复杂度也是log_2 N。并且key不可修改,但是对应的value可以进行修改。

3.针对于key元素,利用迭代器遍历可以得到有序序列。

4.支持[]进行访问。

5.map的key不可重复,multimap的key允许重复。

6.底层使用二叉搜索树-红黑树进行实现。

(注:下述以map为准,multimap使用接口类似)

2.1map的模板参数:

Key、T - 对应键值对中的key-value。之后插入比较均以key类型元素为基准。

Compare - 比较仿函数。默认给的less为库中实现的小于比较。

Alloc - 内存分配

2.2map的迭代器:

 (此时返回的迭代器类型解用引用后是个pair<Key, T>元素)

2.3map的修改:

operator[] - 

访问对应key下标的value值。如果key不存在,那么就进行新的插入,返回值为以T类型调用的默认构造。

at - 访问key下标对应的value值。如果key不存在,则抛异常。

insert - 

 erase - 

2.4map的操作: 

find - 根据传入的元素返回一个迭代器。没有找到返回的就是end()。

count - 根据传入的key返回其出现的对数(对于multimap有效)

2.5代码操作:

void test3()
{
	// map的使用
	map<string, string> m;
	m.insert(pair<string, string>("China", "中国"));
	cout << m["China"] << endl;  // 重载的有[] - []功能强大
	m["Japan"] = "日本";  // 还能直接插入
	cout << m["Japan"] << endl;

	string ar[] = {"西瓜", "苹果", "香蕉", "香蕉", "苹果", "西瓜", "苹果", "香蕉", "西瓜", "苹果", "苹果", "苹果", "西瓜", "苹果", "香蕉"};
	map<string, int> m2;
	for (auto e : ar)
	{
		m2[e]++;  // int() = 0 第一次就是0 + 1 = 1, 存在就++
	}
	auto it = m2.begin();
	while (it != m2.end())
	{
		cout << (*it).first << ":" << (*it).second << endl;
		it++;
	}
	cout << (*m2.find("西瓜")).second << endl;  // 同样的返回迭代器
	cout << m2.find("西瓜")->second << endl;  // 同样的返回迭代器
}

运行结果: 

 

二、基于插入实现的红黑树进行封装

        在了解了基本map和set的基本使用后,我们可以利用之前实现的简单插入实现的红黑树进行封装,文章链接在前言哦~

1.优化红黑树的模板参数

        首先,因为我们实现的红黑树利用的K-Value模型。为了兼容set和map的存储结构(一个只是value、一个是key - value),我们就保留K这个模板参数,利用T这个模板参数一个存储value、一个存储pair<Key, value>。

        那么此时插入数据就是T类型的数据。由于是上层封装,所以传到红黑树这一层,就不知道T类型究竟是value还是一个键值对。此时同样需要上层传入一个仿函数,每次控制对应类型取出对应的key值。set key就是本身,map pair<key, value>->frist。

上层代码初步实现:

map:

namespace YuShen
{
	template<class K, class V>
	class map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)  // 重载()
			{
				return kv.first;
			}
		};
	public:
    // ......
	private:
		RBTree<K, pair<K, V>, MapKeyOfT> _t; // 红黑树结构
	};
}

set:

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

	public:
    // ......
	private:
		RBTree<K, K, SetKeyOfT> _t;
	};
}

下层红黑树代码:

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

// 红黑树结点结构
template<class T>
struct RBTreeNode
{
    //......
};

        此时原本的红黑树key和value模型就混在一起了,需要将所有需要取出key的地方(比如比较的地方)都要利用传入的仿函数对象进行比较,比如:

 2.红黑树的迭代器实现

        在上述的使用中,我们发现map和set都是可以控制迭代器的。实际上,它们调用的就是底层红黑树自己实现的迭代器。

        此时迭代器也就需要自己实现了。首先红黑树是一个二叉树结构,非线性结构,和list的模拟实现类似。

        其次红黑树是一个二叉搜索树。因为控制了所以结点值大于对应结点的左子树的值,小于对应结点的右子树的值的性质,所以对二叉搜索树来说,中序遍历是有序的。那么在接下来的迭代器实现中++或者--就要按照中序的性质进行实现:

红黑树迭代器简单实现:-模板类

模板参数:(STL迭代器三样)class T, class Ref, class Ptr (T T& T*)

重载运算符:

        *解引用        ->箭头访问(指针)        ==/!=        ++ --

解引用、箭头访问、== !=的简单实现:

template<class T, class Ref, class Ptr>  // T T& T*
struct __RBTreeIterator
{
	RBTreeNode<T>* node;
	typedef __RBTreeIterator<T, Ref, Ptr> Self;

	__RBTreeIterator(RBTreeNode<T>* root)
		:node(root)
	{}
	Ref operator*()
	{
		return node->_data;
	}

	Ptr operator->()
	{
		return &node->_data;
	}

	bool operator==(const Self& it) const
	{
		return it.node == node;
	}

	bool operator!=(const Self& it) const
	{
		return it.node != node;
	}
    // ..... ..

}

重载++/--运算符:

        由于实现红黑树的迭代器,当前迭代器存储的结点,要让它指向++后也就是中序的下一个。我们想想中序的时候是如何进行遍历的?左子树,当前结点,右子树。所以,当到此结点后,表示此结点的左子树已经遍历过了,我们就遍历到右子树即可。

        到右子树就要找到右子树中的最左的结点。但是,如果右子树为空呢?按照之前的中序递归方式,此时就会返回到上一层也就是父节点的地方。但是以下面的的一张图为例,有着两种不同的情况:

 

        如果当前结点为1,那么++后就是3结点。 但是如果当前结点为4的话,那么++后的结点就应该是5结点。此时找的条件就是孩子不是父右子树的节点。一直找到空表示此时中序遍历到空了。

        -- 是完全反过来的,首先当前结点,那么右子树已经遍历过了,此时找到左子树的最右结点。如果为空,那么就找到孩子不是父左子树的结点即可。

简单代码实现

	Self& operator++()
	{
		// 中序访问-只不过是碎片化的进行访问
		if (node->_right)
		{
			// 找到右子树最左结点 -- 
			node = node->_right;
			while (node->_left) node = node->_left;
		}
		else
		{
			// 右子树为空,直接访问父节点吗?不一定哦,需要向上找孩子不是父亲的右结点的那个祖先
			RBTreeNode<T>* parent = node->_parent;
			while (parent && parent->_right == node)
			{
				node = parent;
				parent = node->_parent;
			}
			node = parent;
		}
		return *this;
	}

	Self& operator--()  // 反过来了
	{
		if (node->_left)
		{
			node = node->_left;
			while (node->_right) node = node->_right;
		}
		else
		{
			// 右子树为空,直接访问父节点吗?不一定哦,需要向上找孩子不是父亲的右结点的那个祖先
			RBTreeNode<T>* parent = node->_parent;
			while (parent && parent->_left == node)
			{
				node = parent;
				parent = node->_parent;
			}
			node = parent;
		}
		return *this;
	}

3.map、set上层对红黑树迭代器的相关封装:

        在实现了红黑树的迭代器后,就可以具体写出begin和end了,begin就当前红黑树存储的根结点。end只需要给一个空结点过去即可。

         map和set首先对此红黑树类型中的iterator类型进行重命名。由于是通过域访问符::进行访问的,所以编译器编译的时候不好判断其究竟是静态变量还是类型,所以类型的前面就需要加上typename表示这是一个类型以保证编译过的去:

// map		
    typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator;
// set
    typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;

        那么对于插入insert,我们的返回值可以是一个pair,里面存放迭代器和一个bool值。插入失败就返回对应元素的迭代器和false,插入成功就返回插入新结点的迭代器和true值。

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

        对于map来说,此结构还可以方便我们实现[]重载。因为在之前的使用中,我们发现对于map的[],如果对应key数据存在就返回其对应的value值,如果不存在就会创建新的键值对。实际上就是在调用[]调用红黑树的inset,传入key和利用其V的默认构造进行插入,传入失败就说明存在元素,然后返回对应的迭代器的value值即可,插入成功同样返回迭代器的value值即可:

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _t.Insert(pair<K, V>(key, V()));
			return ret.first->second;
		}

        find同样的类似实现,返回迭代器即可。

4.mapset综合简单封装代码:

// map.h
#pragma once
#include "RBTree.h"

// 利用自己实现的红黑树去封装map
// 实验基本功能:数据不冗余,基本实现insert、[]等功能 
namespace YuShen
{
	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;

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

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _t.Insert(pair<K, V>(key, V()));
			return ret.first->second;
		}

		iterator find(const K& key)
		{
			return _t.find(key);
		}
	private:
		RBTree<K, pair<K, V>, MapKeyOfT> _t; // 红黑树结构
	};
}
// set.h
#pragma once

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

	public:
		typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;

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

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

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

		iterator find(const K& key)
		{
			return _t.find(key);
		}
	private:
		RBTree<K, K, SetKeyOfT> _t;
	};
}

另外,加上红黑树中的find实现:

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

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

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

相关文章

java 实现一个最小栈

文章目录最小栈1.实现思路2.实现过程演示3.代码实现思路3.1 压入思路3.2 弹出思路3.3 如何返回栈顶元素的下标3.4 如何返回栈的最小值4.整体代码实现最小栈 1.实现思路 实现一个stack栈 和 minStack栈。先将数据一个一个压入到 stack 中。找到 stack 中的最小值。minStack中始…

简单介绍动态链接过程

文章目录gotgot[0] link_map结构体地址got[1] _dl_runtime_resolvegot[2]之后pltplt[0] 调用libc解析函数plt后面的plt.sec随便拿ida打开一个程序可以看到这是got的内容gdb一下查看内容&#xff0c;可以看到地址是从0开始的大家也知道 got是个独立的section&#xff0c;所以最开…

MySQL数据库(Java的数据库编程:JDBC)

作者&#xff1a;渴望力量的土狗 博客主页&#xff1a;渴望力量的土狗的博客主页 专栏&#xff1a;MySQL数据库 目录 什么是数据库编程&#xff1a; 什么是JDBC? JDBC工作原理&#xff1a; JDBC的使用及相关操作&#xff1a; JDBC开发案例&#xff1a; JDBC常用接口…

关于电影的HTML网页设计-威海影视网站首页-电影主题HTM5网页设计作业成品

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 文章目录一、网页介绍一…

5分钟教你如何设计一个安全web架构

今天就给大家聊聊web安全&#xff0c;web安全占比还是比较大的&#xff0c;基础的从一些html标签&#xff0c;到js 然后到接口&#xff0c;数据库&#xff0c;以及流量攻击&#xff0c;模拟请求。当然这也谈到了一个概念&#xff0c;全新的架构设计模式&#xff0c;前后端分离&…

一文讲解如何学习 Linux 内核网络协议栈

协议栈的细节 下面将介绍一些内核网络协议栈中常常涉及到的概念。 sk_buff 内核显然需要一个数据结构来表示报文&#xff0c;这个结构就是 sk_buff ( socket buffer 的简称)&#xff0c;它等同于在<TCP/IP详解 卷2>中描述的 BSD 内核中的 mbuf。 sk_buff 结构自身并不…

【毕业设计】深度学习人脸性别年龄识别系统 - python

文章目录0 前言1 课题描述2 实现效果3 算法实现原理3.1 数据集3.2 深度学习识别算法3.3 特征提取主干网络3.4 总体实现流程4 具体实现4.1 预训练数据格式4.2 部分实现代码5 最后0 前言 &#x1f525; Hi&#xff0c;大家好&#xff0c;这里是丹成学长的毕设系列文章&#xff0…

【案例分享】华为防火墙出接口方式的单服务器智能DNS配置

介绍出接口方式的单服务器智能DNS的配置举例。 组网需求 如图1所示&#xff0c;企业部署了一台ISP1服务器对外提供Web服务&#xff0c;域名为www.example.com。ISP1服务器的私网IP地址为10.1.1.10&#xff0c;服务器映射后的公网IP地址为1.1.1.10。企业的DNS服务器上存在域名w…

为什么你的用户转化率不高?-- 新媒体运营转化效果渠道归因分析

新媒体运营人最关注的就是流量和用户转化问题。公司发布了新APP、上线了新网站项目&#xff0c;进行用户定位、策划、数据分析和内容营销&#xff0c;花重钱做产品推广&#xff0c;但最后用户转化率却不高&#xff0c;大批用户流失了......这种现象是运营人最不愿意看到的&…

老杨说运维|今年这个会议非比寻常

前言&#xff1a; 人民银行印发的《金融科技(FinTech)发展规划(2022-2025年)》中&#xff0c;重点围绕数字化转型建设&#xff0c;强调上云、数据基础建设以及数智应用的重要性&#xff0c;明确了金融科技的长期重点建设方向。 由金科创新社主办的“2022金融业新一代数据中心发…

kwebio/kweb-core:面向后端的轻量级 Kotlin Web 框架

现代网站至少由两个紧密耦合 的组件组成&#xff0c;一个在浏览器中运行&#xff0c;另一个在服务器上。它们通常用不同的编程语言编写&#xff0c;并且必须通过 HTTP(S) 连接相互通信。 Kweb 的目标是消除这种服务器/浏览器分离&#xff0c;这样您就可以专注于构建您的网站或用…

react多组件出错其他正常显示

问题&#xff1a;一个组件内部有很多个子组件&#xff0c;其中一个出错&#xff0c;怎么实现其他组件可以正常显示&#xff0c;而不是页面挂掉&#xff1f; 一、错误边界 可以捕获发生在其子组件树任何位置的 JavaScript 错误&#xff0c;并打印这些错误&#xff0c;同时展示…

CC攻击和DDOS攻击哪个对服务器影响更大

互联网企业&#xff0c;不管是小企业&#xff0c;还是大企业&#xff0c;大多数企业网站都遭受过攻击&#xff0c;而我们时不时的也能在网上看见某大型企业网站被攻击&#xff0c;崩溃的新闻&#xff0c;网络攻击可以说是屡见不鲜了。攻击力最常见的就是DDOS攻击和CC攻击&#…

使用HTML+CSS技术制作篮球明星介绍网站

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

2022年数维杯国际数模赛浅评

今日数维杯国际大学生数学建模挑战赛将要开赛&#xff0c;为了更好的帮助大家整理了以下注意事项&#xff0c; 竞赛开始时间&#xff1a;北京时间2022年11月17日08:00&#xff08;周四&#xff09; 竞赛结束时间&#xff1a;北京时间2022年11月21日08&#xff1a;00&#xff…

ffmpeg视频编解码 demo初探(二)(包含下载指定windows版本ffmpeg)将YUV图片序列作为流读入,编码封装成x264 MP4视频

参考文章&#xff1a;【FFmpeg编码实战】&#xff08;1&#xff09;将YUV420P图片集编码成H.264视频文件 文章目录第二个项目&#xff1a;将YUV图片序列作为流读入&#xff0c;编码封装成x264 MP4视频将YUV图片序列编码成.h264文件将YUV图片序列编码成mp4文件第二个项目&#x…

艾美捷测序级 II,纯化胰蛋白酶化验程序文献参考

胰蛋白酶是一种基于带正电荷的赖氨酸和精氨酸侧链的底物特异性胰丝氨酸蛋白酶&#xff08;Brown and Wold 1973&#xff09;。这种酶由胰腺排出&#xff0c;参与食物蛋白质的消化和其他生物过程。胰蛋白酶是一种中等大小的球状蛋白&#xff0c;作为一种无活性的胰蛋白酶原产生&…

甘露糖-顺铂mannose-cisplatin|甘露糖-聚乙二醇-顺铂cisplatin-PEG-mannose

甘露糖-顺铂mannose-cisplatin|甘露糖-聚乙二醇-顺铂cisplatin-PEG-mannose 顺铂&#xff0c;又名顺式-二氯二氨合铂&#xff0c;是一种含铂的药物&#xff0c;呈橙黄色或黄色结晶性粉末&#xff0c;微溶于水、易溶于二甲基甲酰胺&#xff0c;在水溶液中可逐渐转化成反式和水解…

基于Feign接口的全链路拦截器

1、前言 单体应用时&#xff0c;我们经常会把一些共享数据&#xff0c;比如登录信息等放在session里面&#xff0c;当然也可以放在ThreadLocal里面。随着业务复杂度的提高&#xff0c;分布式应用越来越主流。单机的存储的思想已经不适用了&#xff0c;共享session应运而生&…

如何度量预测用户付费的误差

在广告&#xff0c;电商&#xff0c;游戏等行业中&#xff0c;预测用户付费是核心的业务场景&#xff0c;能直接帮助提升收入&#xff0c;利润等核心业务指标&#xff0c;堪称预测中的明星。在预测用户付费的系列文章中&#xff0c;结合作者理论和工程实践经验&#xff0c;深入…