改造红黑树实现封装 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

算法奇妙屋(三十四)-贪心算法学习之路 1

算法奇妙屋(三十四)-贪心算法学习之路 1

文章目录 * 一. 力扣 [860. 柠檬水找零](https://leetcode.cn/problems/lemonade-change/description/) * 1. 题目解析 * 2. 算法原理 * 3. 代码 * 二. 力扣 [2208. 将数组和减半的最少操作次数](https://leetcode.cn/problems/minimum-operations-to-halve-array-sum/description/) * 1. 题目解析 * 2. 算法原理 * 3. 代码 * 三. 力扣 [179. 最大数](https://leetcode.cn/problems/largest-number/) * 1. 题目解析 * 2. 算法原理 * 3. 代码 一. 力扣

By Ne0inhk
算法王冠上的明珠——动态规划之路径问题(第一篇)

算法王冠上的明珠——动态规划之路径问题(第一篇)

目录 1. 什么叫路径类动态规划 一、核心定义(通俗理解) 二、核心特征(识别这类问题的关键) 2. 动态规划步骤 状态表示 状态转移方程 初始化 填表顺序 返回值 3. 例题讲解 3.1 LeetCode62. 不同路径 3.2 LeetCode63. 不同路径 II 3.3 LeetCodeLCR 166. 珠宝的最高价值 今天我们来聊一聊动态规划的路径类问题。 1. 什么叫路径类动态规划 路径类动态规划是 动态规划的一个重要分支,核心解决 “从起点到终点的路径相关问题”—— 比如 “路径总数”“最短路径长度”“路径上的最大 / 最小和” 等,其本质是通过 “状态递推” 避免重复计算,高效求解多阶段决策的路径问题。 一、

By Ne0inhk