改造红黑树实现封装 map/set:感受C++ 标准容器的精妙设计与底层实现

改造红黑树实现封装 map/set:感受C++ 标准容器的精妙设计与底层实现

容器map/set的底层是红黑树,这一篇详解红黑树如何封装实现map/set。

1.map/set设计的巧妙之处

map是key/value类型,set是key类型,两个冲突的参数类型,是如何由红黑树封装而成?



暴力思路:两个红黑树,一个kv,一个k。可是这样代码复用率极低,维护成本高



源码思路:利用 键提取器——仿函数 提取kv、k的key,用一颗红黑树实现map,set



C语言一般用函数指针,但是它十分麻烦,C++有了仿函数就很方便

接下来在红黑树基础上封装map和set

2.map和set的实现

2.1map和set的基本框架 + 原红黑树结构变化

map是key、value结构,set是key结构:

 既然我们要用一个红黑树封装实现map和set,那传的参数就得通用:







原本是K,V结构,现在,要改成通用的,就用T吧







T根据需要,可选择传pair<K,V>,也可以是K:(原来是K,V,参数是kv)

然后根据上面,各自写一个仿函数,用于控制红黑树底层获取Key的行为

上层传仿函数,下层需要新增一个模板参数来接收:

2.2Insert

根据上面,原本是KV结构的红黑树,那Insert自然传的是pair

现在传data就行:data类型可能是pair<K,V>,也可能是Key,根据传的不同,创建仿函数对象,去获取它的Key:

Insert后面的部分,反正涉及到获取Key比较大小,就用仿函数

这样就形成一个回调。

2.3Find

Find是用Key比较,那_data类型可能是Key,也可能是pair<K,V>,那同样需要仿函数获取Key

map复用红黑树的Insert,Find



set复用红黑树的Insert,Find

2.4map和set的迭代器Iterator

map和set的迭代器都是由红黑树的迭代器封装而成,源码中有体现。源码写的迭代器十分复杂,包含了继承关系,我们就不写那么复杂。

了解到前面容器的迭代器实现,不难猜红黑树的迭代器也是用一个类封装,再通过重载运算符,使迭代器能像指针一样访问。

这里难点是++和--的实现。红黑树是一种二叉搜索树,走中序遍历输出结果有序。之前使用map和set也提到是这样,得出结论:红黑树迭代器也是走中序遍历:左根右

那begin返回中序遍历第一个结点(最左结点),end返回空结点(最右结点的一个是空,下面)

迭代器实现++ 和 -- ,只需要注意局部。++返回中序下一个节点,--返回上一个,局部解决了,全局就解决了。但是++和--的情况很多,这也就是它的难点,不是逻辑难点,是代码难点。举例子:

根据中序:左根右,可以推断,begin的下一个就是15节点

因为走到10节点,说明当前子树 左,根都遍历完了,就剩右子树,中序访问右子树,就必须先访问其最左节点,15没有最左节点(本身就是右子树的最左节点),那++it的结果就是15节点

这里呢? it遍历到18节点,说明 左与根 都遍历完了,++it就是遍历右子树的最左节点(中序第一个),也就是25节点

那如果右子树为空呢?中序遍历升序,可以知道15的下一个是18,怎么得来?

15的右子树为空,15也已经遍历过,说明当前15的子树遍历完,那15要往上找祖先。15作为10的右子树,说明10也已经遍历完(右子树最后遍历),那10也要往上找祖先。

若10还是祖先的右子树,就还得继续找,找到根的父亲还没找到,或者找到,右父亲,停止。

若10是祖先的左子树,说明才刚遍历完祖先的左子树,那下一个就是该祖先,根

这里同理,++若右子树为空,就得向上找右父亲,25的右父亲直接就是30,得出答案。

2.4.1实现迭代器(重点是++与--)

红黑树内部使用迭代器: 

上层(map和set)复用红黑树的迭代器

模板还未实例化,取其中成员类型时,需要typename:告诉是个类型,后面实例化再去找。

迭代器的 --  和 ++ 完全对称,找左父亲,end前一个为最右结点。

2.5 Key不能被修改的问题

对于set,它的key是不能被修改的,但是这里的迭代器还是能修改它。

可以修改后,这棵树就失去意义了, 所以我们要解决这个Key被修改的问题。

为了让它不被修改,最简单的就是这样:

上层传递的const Key给红黑树,因为被const修饰,所以K将不会被修改,从源头解决问题。

typedef这里的声明也需要加上const,不然会出现很多模板特有的意义不明确的报错。

对于map,pair里的Key也不容被修改,那就同理:

2.6修改Find返回值为Iterator,实现统计次数功能

2.7修改Insert返回值为pair<Iterator,bool>,实现operator[ ]

map和set那节,讲过这个[ ]的使用和底层

newnode保存新增结点cur(因为插入过程设计旋转加变色,向上更新,最后的cur不一定是新增)

解决完Insert,接下来就实现operator[ ]:

首先插入Key,如果插入失败,说明存在,flag为false,it是已存在节点,最后返回value

                        如果插入成功,说明不存在,flag为true,it是新增结点,最后返回value

外层对value([ ]的返回值)++,即可统计次数,不用那么麻烦。

j

2.8红黑树的析构函数

map和set的析构函数会调用红黑树的析构,所以不用写他们的。注意后序遍历析构

Read more

从零开始打造高性能数据结构——手把手教你实现环形缓冲

从零开始打造高性能数据结构——手把手教你实现环形缓冲

◆ 博主名称: 小此方-ZEEKLOG博客 大家好,欢迎来到小此方的博客。 ⭐️个人专栏:《C语言》_小此方的博客-ZEEKLOG博客 算法_小此方的博客-ZEEKLOG博客  ⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰。 目录 一,普通队列的劣势 1. 空间浪费严重(“假溢出”问题) 2. 需要频繁移动元素(若避免浪费) 3. 扩容成本高 4. 无法解决“假溢出”导致的提前扩容 二,环形缓冲结构分析  1. “循环”取模实现指针回绕  2.“循环”,轮流入座而不是排长队 三,实现环形缓冲 1,MyCircularQueue(k): 构造器   1,结构体搭建   2,初始化 3,为什么选择k+1块空间而不是k块空间?

By Ne0inhk

优选算法——前缀和

👇作者其它专栏 《数据结构与算法》《算法》《C++起始之路》 前缀和相关题解 1.前缀和 算法思路: a.先预处理出来一个【前缀和】数组:         用dp[i]表示:[1,i]区间内所有元素的和,那么dp[i-1]里面存的就是[1,i-1]区间内所有元素的和,那么:可得到递推公式:dp[i]=dp[i-1]+arr[i]; b.使用前缀和数组,【快速】求出【某一个区间内】所有元素的和:         当访问的区间是[l,r]时:区间内所有元素的和为:dp[r]-dp[l-r]。 #include <

By Ne0inhk
蓝桥杯C++组算法知识点整理 · 考前突击(上)【小白适用】

蓝桥杯C++组算法知识点整理 · 考前突击(上)【小白适用】

【背景说明】本文的作者是一名算法竞赛小白,在第一次参加蓝桥杯之前希望整理一下自己会了哪些算法,于是有了本文的诞生。分享在这里也希望与众多学子共勉。如果时间允许的话,这一系列会分为上中下三部分和大家见面,祝大家竞赛顺利! 【文风说明】本文主要会用代码+注释的方式来解释内容。相信学过编程的人都会发现程序比长篇大论更易理解! 目录 一、语言基础 1.1 编程基础 1.2 竞赛常用库函数 1.2.1 sort 函数 1.2.2 最值查找 1.2.3 二分查找 1.2.4 大小写转换 1.2.5 全排列 1.2.6 其它库函数整理 1.3 STL的用法 1.

By Ne0inhk
【PyTorch】2024保姆级安装教程-Python-(CPU+GPU详细完整版)-

【PyTorch】2024保姆级安装教程-Python-(CPU+GPU详细完整版)-

一、准备工作 1. pytorch需要python3.6及以上的python版本 2. 我是利用Anaconda来管理我的python。可自行安装Anaconda。 3. Anaconda官网 Free Download | Anaconda 具体Anaconda安装教程可参考 https://blog.ZEEKLOG.net/weixin_43412762/article/details/129599741?fromshare=blogdetail&sharetype=blogdetail&sharerId=129599741&sharerefer=PC&sharesource=2201_75436278&sharefrom=from_link 二、pytorch介绍 安装 PyTorch 时,可以选择在 CPU 或

By Ne0inhk