当前位置: 首页 > news >正文

C++语法——map与set的封装原理

目录

一.数据类型封装

(一).封装方式

(二).封装后如何取key比较

 二.迭代器封装

(一).底层迭代器(红黑树中)

①迭代器++

②迭代器-- 

(二).begin&end&const


一.数据类型封装

(一).封装方式

我们知道,map与set所使用的都是红黑树,但是map是key&value模型,set是key模型。

map需要传两个类型而set只用传一个类型,这在底层红黑树中该怎么兼容呢?

透过SGI版stl_tree.h我们可以看到,红黑树有两个模板参数分别表示key和value。重点来了:

对于map而言需要传入key和pair<key, value>分别给红黑树的key和value。

set需要传key和key给红黑树的key和value。

再看map与set源码(进行部分截取):

 如果自制的话,大致如此:

template<class K, class V>
class Map
{
	typedef rb_tree<K, pair<K, V>, MapOfV<K, pair<K, V>>> tree;
public:
	//...

private:
	tree _t;
};
template<class K>
class Set
{
	typedef rb_tree<K, K, SetOfV<K>> tree;
public:
	//...

private:
	tree _t;
};

(二).封装后如何取key比较

但是这种封装有一个问题,就是在底层红黑树中,是直接拿value进行比较,那这该怎么办呢?

仿函数取value中key值来解决。

对于map而言,传仿函数给红黑树,调用仿函数取value(实际是pair<key, value>)中的key值

对于set而言,key和value中的值实际上都是key,也传仿函数取即可。

因此,对于上述代码我们看到map和set分别传入了仿函数MapOfV和SetOfV。

仿函数均在上层中(map/set)实现,传入底层红黑树。

代码实现如下(简易):

template<class K, class V>
//传入分别是key和pair<key, value>
//typedef rb_tree<K, pair<K, V>, MapOfV<K, pair<K, V>>> tree;
class MapOfV
{
public:
	K operator()(V kv) const
	{
		return kv.first;
	}
};
template<class K>
//typedef rb_tree<K, K, SetOfV<K>> tree;
class SetOfV
{
public:
	K operator()(K k) const
	{
		return k;
	}
};

 二.迭代器封装

因为map和set底层都是红黑树,因此迭代器实现在红黑树中(底层),而map和set只是调用红黑树的迭代器

(一).底层迭代器(红黑树中)

因为红黑树也是由一个个节点组成,这就很容易想到list的迭代器。list迭代器是用类封装,迭代器内部定义一个指针指向节点,对迭代器的操作底层是对迭代器内部指针的操作。

红黑树的迭代器与之类似,也是使用一个类来表示迭代器,内部有一个指向树节点的指针。只要了解过list迭代器都能想出来,不了解可以看看这篇文章哦:为什么List的迭代器是类模板?

对于解引用*和箭头->的封装与list相同,直接转化成对节点指针的操作。

关键在于++和--。 

①迭代器++

红黑树也是二叉树,我们就直接以二叉树为例:


 这里要分两种情况: 

 第一种:节点有右子树,此时要向下走。

以1号节点为例,因为有右子树,++后就是6号节点?——错!

二叉树迭代是前序遍历的方式,++后应该是5号节点,即右子树的最左节点!

第二种:节点没有右子树,此时要向上走。

对比3号和5号节点,都是叶子节点,但是3号节点++后是1号节点,5号++后是6号节点。

得出结论:如果当前节点是父节点的左子树,那++后就是父节点如果是右子树,那么需要向上找直到当前节点是父节点左子树,此时父节点是++后节点。

如果是节点7那么++会一直判断到根节点4,因此,迭代器end()可以设置为root的父节点(null)。

原理很简单,如果是右子节点那说明对于父节点而言你就是++后会找到的节点,但是对于自己而言++后就不可能是父节点。

代码如下:

Self operator++()
{
	goBack();
	return *this;
}
void goBack()
{
	if (_node->right)//如果右子节点不为空
	{
		Node* right = _node->right;
		while (right->left)//直到找到右子树最左节点
		{
			right = right->left;
		}
		_node = right;
	}
	else//右子树为空
	{
		Node* parent = _node->parent, * cur = _node;
		while (parent && cur == parent->right)//直到cur是parent的左子节点为止
		{
			cur = parent;
			parent = cur->parent;
		}
		_node = parent;
	}
}

②迭代器-- 

有了++的理解,--操作就简单了。

还是以该树为例:
 也要分两种情况:
第一种:有左子树,向下走。

以根节点4为例,--之后不是节点2而是节点3。对于有左子树的节点而言,--之后的节点是左子树的最右子节点。

第二种:没有左子树,向上走。

对比节点3和节点5,节点3--之后是节点2,节点5--之后是节点4。可以看出,如果节点是父节点的右子节点,那么--之后就是父节点。但如果是左子节点,那么需要向上判断直到是父节点的右子节点为止。或者如节点1一样,一直判断到根节点为止。

其原理就是如果是父节点的左子节点,那么说明父节点在自己之后,如果是右子节点那么说明在父节点之后。

代码如下:

Self operator--(int)
{
	Self it(_node);
	goHead();
	return it;
}
void goHead()
{
	if (_node->left)//有左子树
	{
		Node* right = _node->left;
		while (right->right)//直到左子树的最右子节点
		{
			right = right->right;
		}
		_node = right;
	}
	else//无左子树
	{
		Node* parent = _node->parent, * cur = _node;
		while (parent&& cur = parent->left)//直到cur是parent的右子节点
		{
			cur = parent;
			parent = cur->parent;
		}
		_node = parent;
	}
}

(二).begin&end&const

迭代器的begin和end只能在rb_tree中实现,对于begin而言,返回红黑树最左节点的迭代器,end返回root的父节点迭代器(空节点迭代器)。

对于const,首先我们应该知道不管map还是set均不支持对key的修改

但在实现上map和set大有不同

map采用在传入时就传入const key,而set采用将迭代器都使用底层const迭代器

用几个小时来制定计划,可以节省几周的编程时间—— 未名


如有错误,敬请斧正

相关文章:

  • 搭建gataway鉴权流程
  • Codeforces Round #835 (Div. 4)A.B.C.D.E.F
  • flask入门教程之数据库保存
  • 网站变灰白css
  • Template类创建模板替换字符串
  • MySQL日志(undo log 和 redo log 实现事务的原子性/持久性/一致性)
  • (论文阅读笔记)Network planning with deep reinforcement learning
  • 培养出最多亿万富翁的美国大学TOP10榜单
  • 蓝桥杯嵌入式AD采样解析
  • 数据结构和算法——基于Java——4.1栈(数组实现栈、链表实现栈)
  • 怎么看网站域名有没有收录 收录情况怎么样 网站收录查询
  • 信号发生器不会用?一篇文章教会你
  • Java+JSP+MySQL基于SSM的医院挂号就诊系统-计算机毕业设计
  • 今年十八,喜欢ctf-web
  • AI加速(九): 深度理解吞吐量和延时
  • 【跃迁之路】【641天】程序员高效学习方法论探索系列(实验阶段398-2018.11.14)...
  • android百种动画侧滑库、步骤视图、TextView效果、社交、搜房、K线图等源码
  • CSS相对定位
  • es6(二):字符串的扩展
  • Git同步原始仓库到Fork仓库中
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • Java的Interrupt与线程中断
  • laravel5.5 视图共享数据
  • magento2项目上线注意事项
  • nfs客户端进程变D,延伸linux的lock
  • PaddlePaddle-GitHub的正确打开姿势
  • Shadow DOM 内部构造及如何构建独立组件
  • Spark RDD学习: aggregate函数
  • 技术攻略】php设计模式(一):简介及创建型模式
  • 聚簇索引和非聚簇索引
  • 正则与JS中的正则
  • 进程与线程(三)——进程/线程间通信
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​sqlite3 --- SQLite 数据库 DB-API 2.0 接口模块​
  • !$boo在php中什么意思,php前戏
  • # C++之functional库用法整理
  • #Linux(Source Insight安装及工程建立)
  • #我与虚拟机的故事#连载20:周志明虚拟机第 3 版:到底值不值得买?
  • $ git push -u origin master 推送到远程库出错
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (Bean工厂的后处理器入门)学习Spring的第七天
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (小白学Java)Java简介和基本配置
  • (学习日记)2024.04.04:UCOSIII第三十二节:计数信号量实验
  • (一)Thymeleaf用法——Thymeleaf简介
  • (一)使用IDEA创建Maven项目和Maven使用入门(配图详解)
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • .gitignore文件—git忽略文件
  • .net 8 发布了,试下微软最近强推的MAUI
  • .Net Core和.Net Standard直观理解
  • .NET 反射 Reflect
  • .NET 设计模式初探
  • [ASP.NET 控件实作 Day7] 设定工具箱的控件图标
  • [BZOJ2208][Jsoi2010]连通数
  • [C puzzle book] types