这一篇是红黑树。
目录
2.4红黑树的常用接口:size,height,InOrder
1.红黑树的概念(定义,规则,效率)
红黑树是一颗平衡二叉搜索树,他的每个结点增加一个存储位来表示结点的颜色,红色,或黑色。通过约束任意一条从根到叶子的路径的结点颜色,红黑树确保了没有一条路径比其他路径长两倍,因此它的平衡的。
1.1红黑树的规则
1.每个结点不是红色,就是黑色。
2.根节点是黑色的。
3.如果一个结点是红色的,那与它直接相连的必须为黑色。也就是说:一条路径中不会出现连续的红色结点。
4.黑色结点可以连续存在
5.从任意一个节点(或同高度的节点,根节点)到其后代 NULL 叶子节点的所有路径上,包含的黑色节点数量完全相等
6.最短路径bh(blackheight,黑高。全黑,不用黑红交替,就是最短,所以是黑高),最长路径不会超过(可以等于)2bh(全部红黑交替)(注意,算黑高,算路径,必须包含NULL结点,算普通的层数高度,不需要算NULL结点) ,易推导:任意一条路径高h(包含NULL),bh<=h<=2bh
7.红黑树中所有路径的统计必须以黑色的 NULL 叶子结点为终点,它必须是黑色的。
7.图示

4.图示

1.2红黑树的效率
假设N为红黑树中结点数,h为最短路径,可推出: 。由此:
,那红黑树增删查,最差也就是
,那它的时间复杂度就是
。
红黑树相比AVL树更抽象,AVL树通过控制高度差,直观地控制了平衡。红黑树通过多条规则约束,间接的实现了平衡。他们的效率都是同一档次,log N,但是相对的,插入结点数相同,红黑树旋转次数更少,因为它可以避免不必要的旋转(单纯修改颜色就行,实现底层具体说明)
2.红黑树的底层实现
2.1红黑树的基本框架
这里用枚举来定义颜色,在类中加入一个新的变量,用来存储结点颜色,其余类似。
//枚举表示颜色
enum Colour
{
BLACK,
RED
};
// 这里我们默认按key/value结构实现
template<class K, class V>
struct RBTreeNode
{
// 这里更新控制平衡也要加入parent指针
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
private:
Node* _root = nullptr;
};
2.2红黑树的插入
红黑树插入一个值的过程:
1.因为底层是二叉搜索树,所以插入过程与二叉搜索树一致。插入后观察是否符合红黑树规则
2.如果是空树插入,新增结点是黑色结点。若是非空树插入,新增结点必须是红色结点。若新增黑色结点,会破坏规则,并且黑色结点很难处理(黑高失衡,增加一个黑色,会影响经过这结点的所有路径的黑高,牵一发而动全身,一个个去修复,很难。)。
3.非空树,新增结点必须是红色结点,若父亲是黑色结点,则没违反规则,插入结束。
4.非空树,新增结点必须是红色结点,若父亲是红色结点 ,因为有连续红色结点 ,所以破坏了规则 ,进一步分析:c是红色,p是红色,那g必为黑(插入此节点之前,肯定是满足之前规则的,所以g和p不能构成连续红色),这三个颜色都固定了,此时只需要关注u的颜色变化:
情况1:变色:条件:u存在且为红
情况1:变色:条件:u存在且为红 ,只需要将p,u变黑色,g变红色(g往下的路径,黑色结点数量还是不变,子树整体不受到影响,外层视作无变化,内部结构变了)。若g的p为黑色,无连续红色,结束更新。反之继续往上更新(c变成g,对应关系都变化)。若更新到了根,则不需要向上更新(g->p为空),把(g)根节点改为黑色。
无论c是p的左右,p是g的左右,变色处理方式一致。因为本质是处理连续的红色结点和黑色结点数量不平衡问题,不需要旋转,无关左右。

下图是抽象图,子树内部情况有多种,bh为2时,情况已组合到上百亿级别,所以既然处理方式一样,那就不徒增麻烦了。
情况2:单旋+变色:条件:u不存在或存在且为黑
u不存在,则c一定是新增结点(如果u不存在,那c存在的话,c之前肯定是符合要求的,导致c是黑色结点,但是如果c是黑色,左子树多了一个黑色结点数量,导致黑高失衡,所以c肯定是新增结点)
u存在且为黑,c一定不是新增结点(如果已存在,c必然是黑色结点,但却是红色,所以:c一定是之前符合情况1,向上更新的途中,由g变为c的,变色过后的。)
分析:p必须变黑,才能解决连续红色结点。u不存在,或存在且为黑,也就是无法变黑,那必然导致黑高失衡。此时单纯变色(怎么变色,都是少一个黑高)无法解决问题,需要旋转+变色:
旋转又分左旋,右旋:(左右镜像,只讲一种)

对g结点进行右旋或左旋,"压下去"一层高度,再把p变黑,g变红。

当u存在且为红,c是黑色变来的。之前分析过,该子树黑高是不变的,c的黑色转移到抽象的子树中了(pu变黑,g变红,通过g的黑高没变化),所以别看具体是右边黑结点多一个,其实左边的转移到抽象子树中了。

下图证明一下:

情况3:双旋+变色:条件:情况2的基础上,不是单纯的一边高
一图知晓:

先对p左旋,再对g右旋。第一个左旋的目的是恢复成情况2。


插入的完成代码展示:



2.3红黑树的查找
与二叉搜索树完全一致。 效率O(log N)

2.4红黑树的常用接口:size,height,InOrder

递归实现,十分简单。


2.5红黑树的平衡验证
可不可以通过获取**:最短路径bh与最长路径2bh验证平衡** ?答案是不行。因为满足这个不一定是红黑树,但是红黑树一定满足。所以要利用红黑树的根本逻辑(颜色)
1**.前序遍历,递归检查每个节点左右子树的黑高。**
2.还可以检查是否有连续红色:root为红,root父存在且为红,直接返回false(查找孩子是否是红色结点很难,因为可能不存在,但是查找父亲就容易,所以查找父亲来判断)
3.当前root是黑结点,定义变量记录黑结点++root_cur_bn
4.根据:从根到任意叶子结点的路径上,黑色结点数量必须相同 ,记录最左路径,作为基准,传参,当前序遍历,走完一条路径时,当前路径黑结点也统计完,比较一下基准,看看是否相同,不相同返回false,不满足规则。

为啥递归到空,就可以检查基准,并且确保了每条路径都检查到了呢?
因为第一次递归肯定是最左路径,然后又会递归最左的结点的右结点,此时是新路径,这两条递归会去最左结点的父亲结点p,然后p结点递归右,那就是每次递归到空,都是遍历了一个路径,所以可以在为空时判断,并且判断的很全面,会覆盖每条路径。
当然这样性能损耗也不小,要遍历全部,时间复杂度O(N).
前中后序递归,都会递归遍历全部路径,只是顺序不同。
前序适合这个检查校验,因为前序是先统计,校验后,再逐次往下检查。
但是中序和后序,都需要"折返",先折腾,然后回来统计,所以不适合这里的校验。

内容结束
