数据结构之树从入门到如土(四)----如何看待本文教你会十分钟学会手写一个红黑树

红黑树的历史

红黑树(英语:Red–black tree)是一种不平衡二叉查找树,
无需保证左右子树高度差小于等于1。
是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
它在1972年由鲁道夫·贝尔发明,被称为"对称二叉B树",
它现代的名字源于Leo J.Guibas和RobertSedgewick于1978年写的一篇论文。
红黑树的结构复杂,但它的操作有着良好的最坏情况运行时间,
并且在实践中高效:它可以在O(logN)时间内完成查找,插入和删除,
这里的n是树中元素的数目。

红黑树定义?

几乎所有的 红黑树的教程,都会搬上这五条定义,但实际上没人能一开始就明白了可能学完后也不明白为什么 满足这五条性质就能实现一颗平衡二叉树,那么照例 我也把这无聊性质 搬上来。

  1. 每个节点或是红色的,或是黑色的。
  2. 根节点是黑色的。
  3. 每个叶子节点(NIL)是黑色的。
  4. 如果一个节点是红色的,则他的两个子节点都是黑色的。
  5. 对每个节点,从该节点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
红黑树实际应用:
  1. IO多路复用epoll的实现采用红黑树组织管理sockfd,以支持快速的增删改查。
  2. ngnix中,用红黑树管理timer,因为红黑树是有序的,可以很快的得到距离当前最小的定时器。
  3. java中TreeMap,jdk1.8的hashmap的实现。
红黑树的直观感受

演示网站:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
在这里插入图片描述
首先在深入了解前,我们 首先先直观的感受下 红黑树的特点, 你可以看到红黑树是 红黑相间的,就好像 老师每次 让排队时,你只要认识你的左右 每个人 通过 你左右的约束 就完成了一个有序的结构,红黑树 是 通过上面 5条性质的约束,来保持它这一性质。

关于红黑树的疑问

首先 我在 学习红黑树的时候产生了 3个疑问?
6. 红和黑他们代表了什么对这颗树产生了什么样的影响。
7. 如果不去看红和黑,那么这棵树 还有怎样的特点。
8. 为什么是红黑树,而不是红黑绿树 或者 红橙黄绿青蓝紫树,我这里 并不是说 为什么 颜色 以 红黑命令,毕竟 很多东西的命名 不是 作者的名字就是作者当时临时起意 喜欢喝 咖啡就 有了Java,喜欢 啃苹果 就有了Apple(😝)。好了有点扯远了,我的疑问是 为什么 用抽象上的2 种节点状态(红黑),如果用3种或者N种节点的状态有什么不同,关于这个问题可能就算理解红黑树可能也不一定能解答,但 从哲学的角度 道生一一生二二生三三生万物 也就是 只要红黑2种状态就能抽象的表示 N多种情况 就是2进制 数能表示 10进制 16进制 或者N进制,而且只用这两种状态 写法是最简单的 一旦节点状态变成3种或N种 索要处理的情况可能就会变得更复杂毕竟 红黑树在添加时就已经需要处理十几种情况了,不过 我们算法 的条件越多从信息论的角度 也就是信息熵越多 确定性 越多 所需处理的不确定性的代价也就越小(当然前提 这些条件 不是相互冗余的 不能相互推导出对方),红黑树的这五条定义 注意是定义 就是独立的,相互之间 不能推导。

对于前 2个 问题 我们还是可以 用画图的方法 进行分析的,人的记忆就像排序一样 如果你不了解这个东西 和 其他东西的区别 你就不能找到合适的位置 和其他知识点在你大脑形成链表(关联记忆)。

红黑树的应用

红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。

红黑树的时间复杂度和相关证明

这里咱 先引用算法导论的 证明,抽空 我在用自己语言表述下.
红黑树的时间复杂度为: O(lgn)
下面通过“数学归纳法”对红黑树的时间复杂度进行证明。

定理:一棵含有n个节点的红黑树的高度至多为2log(n+1).

证明:
“一棵含有n个节点的红黑树的高度至多为2log(n+1)” 的逆否命题是 “高度为h的红黑树,它的包含的内节点个数至少为 2h/2-1个”。
我们只需要证明逆否命题,即可证明原命题为真;即只需证明 “高度为h的红黑树,它的包含的内节点个数至少为 2h/2-1个”。

从某个节点x出发(不包括该节点)到达一个叶节点的任意一条路径上,黑色节点的个数称为该节点的黑高度(x’s black height),记为bh(x)。关于bh(x)有两点需要说明:
第1点:根据红黑树的"特性(5) ,即从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点"可知,从节点x出发到达的所有的叶节点具有相同数目的黑节点。这也就意味着,bh(x)的值是唯一的!
第2点:根据红黑色的"特性(4),即如果一个节点是红色的,则它的子节点必须是黑色的"可知,从节点x出发达到叶节点"所经历的黑节点数目">= “所经历的红节点的数目”。假设x是根节点,则可以得出结论"bh(x) >= h/2"。进而,我们只需证明 "高度为h的红黑树,它的包含的黑节点个数至少为 2bh(x)-1个"即可。

到这里,我们将需要证明的定理已经由
“一棵含有n个节点的红黑树的高度至多为2log(n+1)”
转变成只需要证明
“高度为h的红黑树,它的包含的内节点个数至少为 2bh(x)-1个”。

下面通过"数学归纳法"开始论证高度为h的红黑树,它的包含的内节点个数至少为 2bh(x)-1个"。

(01) 当树的高度h=0时,
内节点个数是0,bh(x) 为0,2bh(x)-1 也为 0。显然,原命题成立。

(02) 当h>0,且树的高度为 h-1 时,它包含的节点个数至少为 2bh(x)-1-1。这个是根据(01)推断出来的!

下面,由树的高度为 h-1 的已知条件推出“树的高度为 h 时,它所包含的节点树为 2bh(x)-1”。

当树的高度为 h 时,
对于节点x(x为根节点),其黑高度为bh(x)。
对于节点x的左右子树,它们黑高度为 bh(x) 或者 bh(x)-1。
根据(02)的已知条件,我们已知 “x的左右子树,即高度为 h-1 的节点,它包含的节点至少为 2bh(x)-1-1 个”;

所以,节点x所包含的节点至少为 ( 2bh(x)-1-1 ) + ( 2bh(x)-1-1 ) + 1 = 2^bh(x)-1。即节点x所包含的节点至少为 2bh(x)-1。
因此,原命题成立。

由(01)、(02)得出,“高度为h的红黑树,它的包含的内节点个数至少为 2^bh(x)-1个”。
因此,“一棵含有n个节点的红黑树的高度至多为2log(n+1)”。

红黑树 和 2-3树的关系
2-3-4树
定义

2-3-4树是四阶的 B树(Balance Tree),它的结构有以下限制:

所有叶子节点都拥有相同的深度。
节点只能是 2-节点、3-节点、4-节点之一。

2-节点:包含 1 个元素的节点,有 2 个子节点;
3-节点:包含 2 个元素的节点,有 3 个子节点;
4-节点:包含 3 个元素的节点,有 4 个子节点;

元素始终保持排序顺序,整体上保持二叉查找树的性质,即父结点大于左子结点,小于右子结点;而且结点有多个元素时,每个元素必须大于它左边的和它的左子树中元素。

在这里插入图片描述
2-3-4树的查询操作像普通的二叉搜索树一样,非常简单,但由于其结点元素数不确定,在一些编程语言中实现起来并不方便,所以红黑树 便是用二叉树的结构引入了 红黑的概念。

从上图 可以看到 2-3-4树 在 1阶情况下就是普通的二叉树,在 3阶 情况下 就等价于红黑树。 在 4阶的二叉树下子树变成了4颗,这样做的好处是树的深度就会减小,从而调整树的平衡操作也会变少 这样在大量插入的操作上 相比AVL 也变得更高效。

值得注意的是,红黑 属性在 节点 内表示但 其实它的 含义是应该 是引出一条边 出过红色相连 的边 就表示 咱们 是 一个层级的。

为什么需要3个节点的原子操作。

在AVL树里面我们经常 可以看到对 3个节点的 左旋右旋等操作,红黑树里面也有,在红黑树里面 通过左右旋 可以保持节点 黑节点的 平衡,其实 不管AVL树 或者 红黑树 都是同 通过找到最小的结构 通过它们之间关系的定义(5条性质) , 来描述出 红黑树这样的数据结构。 而 在红黑树立 发明者 通过 父节点、祖父节点、叔叔节点、自己 这样 4个抽象节点,通过它们 之间 定义的关系,实现了 对普通二叉树 的升级。在我看来 红黑树就好像碳元素 通过一定的 排列 就能展展现出 钻石的性质,这无疑是非常美妙的。

还有提及一点 在AVL树里面 为什么 3个节点的原子操作就能保持树的高度呢?在红黑树里面我们主要调整的是黑高度。如果 高度差 相差一 那么 你想想 怎么调整呢。
很简单的想法 减去 一层的高度 不就好了 怎么减呢 把高度高的往上移动 一层 不就好了吗, 所以 要移动的层和被移动到的层 之间 高度差至少为 1,父节点 和子节点 不就 高度差相差 1 吗 那么 你把自己提上去 替代父亲 不就能把 高度差不上了吗。所以为什么 是 3个是最小单位 可以想明白吧,首先 你的高度要 要比平级的层 高一层 那么 你和你 平基层 也就是兄弟节点 的关系至少是 如果你兄弟是 高度 是 x 你的高度至少要是 x + 1 也就是 1 那么 1的高度下面 那么你至少要有一个节点 所以 最少的情况下
在这里插入图片描述
你至少 底下 要有 一个节点,此时 你才能满足不平衡的条件才需要往上提一层。然后
你得知道 父节点放在哪,你要知道父节点是比你大的,所以 如果塞在你下面 首先不能塞在 你下面 高度比较高的 那一边, 要不然 等于白操作 所以 你就需要看 了 如果是上图这种情况 你塞在 ? 那块 高度 不会超过 左边 而且 将 父节点比我大 转化为了 我的右孩子比我大 都是比我大 就很好的满足了 性质 。所以嘛这样就很好 但是 想法是美好的 可能还有一种情况, 如果
在这里插入图片描述
我下面节点 是挂在 右边的 ,那怎么办呢? 不能直接把 父节点 塞在我 右边 啊 ,塞进去了 高度 不合适啊,就像上图展示的,如果把 父节点塞到我的右边 性质是满足了 父节点比我大,但是 高度还是没解决 那怎么办 再循环这个步骤把 然后把我的儿子往上提 我再 往我儿子的右边塞 循环往复 那就成了死循环了,其实向也很简单 把让 右边的节点高度减去 1 嘛, 那怎么减吗 这个问题是不是 很熟悉 这就是 我们在讨论的问题 让某一层高度减去一 ,那我们 可以复用上面的思想 把 儿子的 右边的节点 也是用一次 旋转 不就可以了吗 只要他下面 的 右节点 的高度没有左节点的 高 那么 我们 就可以直接使用一次旋转,那么有人要问了 如果右边大于 呢 上边不是说过了吗 把右边的减掉一层的高度 具体的 就是 左旋或者右旋 一下。所以实际上 我们解决问题 是不断压缩问题的规模 让他 变成了 只要左旋 或右旋 就能解决问题,这就是递归 的思想了.上面 我们只是 提到了左旋 其实右旋操作是一样的。
总结:其实 实际上 AVL 树 和红黑树的旋转 都是 消除了 左边和右边1 的高度差,在红黑树里面 我们消除的是 黑色节点产生的高度差,所以 在判定条件里面 为什么 我们 要在叔叔节点是黑色情况下 才需要旋转 ,因为 从祖父节点开始 途径的黑色节点数量不一致了,所以我们才需要旋转。

图解 红黑树 插入 和 2-3-4 树 对比

所以 所谓 红黑树 就是 2-3-4 树 的四阶情况下 使用了 二叉树的结构进行 的一种实现。
在这里插入图片描述
上图 我们进一步的 可以看到 二三树 和 红黑树的关系,红色 的树节点表示 它和它的父节点 是 同一层的 高度是一样的(在2-3-4树中).
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

红黑树杂谈

首先我们 来看看 为什么 叔叔节点 是黑色节点 我们就要 左旋或者 右旋呢?

在这里插入图片描述
首先 红黑树 的思想 是由 2-3-4 树 作为参考的,那么 2-3-4 树 是严格平衡的,如果遵循 红黑树的 五条性质 那么 就会出现以上这种结构, 也就是 插入红色节点的 叔叔节点是nil 这种情况,仿照 右边的 2-3-4树 你就能看出 这种情况 是不能出现的因为 此时 2-3-4 树左边的高度 和右边 高度就不一致了。
在 红黑树里面 2-3-4 树 这种性质体现在 从任何一个节点 往下走 经过 的黑节点 是一样的。上面 从 根节点往下走很明显 如果不算 nil的话,左边是 2 右边是 1 很明显就不符合2-3-4 树在红黑树里面的 特征了。
所以我们通过一次 左旋操作,将它 转换成了 符合定义的样子,所以我们可以看出定义5 是来自于,2-3-4 树在 红黑树的性质。
在这里插入图片描述
由小见大,对红黑树的旋转 基本上都可以认为是为了保持 黑节点数量的一致这条性质。

那么 我们再分析 第二大类 其实 第二大类 情况 也是维护的 是 2-3-4 树的情况 只要你熟悉2-3-4树 你就知道 当你要插入节点时 如果要被插入的节点满了,它会往上 合并
我觉得 如果类比的话 类似于 进位

红黑树的基本数据结构

与AVL树 最大区别 是我们首先 不需要 高度这个属性,但同时 需要加入 颜色 和 父节点 这两个属性,为什么加入父节点属性 是方便向上 溢出 也就是调整颜色,需要父节点否则没法找到父节点,由于颜色只有红和 黑 为了省内存空间 可以用 布尔型表示。

//红黑树结构
type RBNode struct {
	Left   *RBNode //左节点
	Right  *RBNode //右节点
	Parent *RBNode //父节点
	Color  bool    //颜色
	Data           //key
}

type RBtree struct{
	Root *RBNode //树的根节点
	count uint	//树节点计数
	NIL *RBNode	//用来保存一个nil 节点 方便写程序
}

还需要注意的是,我们 假设 每个插入节点的左节点 和右节点 都默认为nil 节点 nil节点
就是一个RBNode 除了颜色为黑 其他属性都没有, 这么做可以方便我们 编写程序,因为 在红黑树里 我们把叶子节点下面 的nil 看做是黑色,插入时 我们需要作判断条件。

为了 能让 所有类型的数据 都能 插入Go 语言里没有泛型,我们使用 Inteface{} 作为泛型 传入任何类型 ,同时 传入的键 一定 得定义 一个能比较的方法 否则 没法 排序,

//处理整形类型的比较函数
type Int int
func (x Int) Less(than Item)bool{
	return x < than.(Int)
}
//处理uint32类型的比较函数
type UInt32 uint32
func (x UInt32) Less(than Item)bool{
	return x < than.(UInt32)
}
//处理字符串类型的比较函数
type String string
func (x String) Less(than Item)bool{
	return x < than.(String)
}
//红黑树 颜色 用布尔值表示
const (
	RED = true
	BLACK = false
)
//数据 必须能比较 所以需要定义 实现方法
type Data interface {
	Less(than Data)bool
}
红黑树算法 复杂度证明

为什么 我们使用红黑树,因为它比较快,那怎么证明呢?

红黑树的 左旋右旋

和AVL左右旋 一样 但是 但在 红黑树里面 我们新增了Parent 用于向上遍历对颜色进行校正,所以 在 节点调整位置的时候 还得维护Parent的指针。

左旋

在这里插入图片描述

//左旋转 和AVL树的旋转 是一样的 但是 我们旋转的同事还对parent 父节点
//的指针进行维护
func (rbt *RBtree) rbleftRotate(root *RBNode){
	//如果 根节点的右边为nil节点 那么就无法进行左旋 直接返回
	if root.Right == rbt.NIL {
		return
	}
	//左旋 找到 右边的往上提
	tmp := root.Right
	root.Right = tmp.Left
	//判断左边节点是否存在 更新 左边的节点的父节点为root
	if root.Right != rbt.NIL{
		root.Right.Parent = root
	}
	//修改旋转后的父节点
	tmp.Parent = root.Parent
	//判断 父节点的 如果旋转的是根节点 就吧根节点 设置为 旋转后的节点
	if root.Parent == rbt.NIL {
		//如果是 旋转前的root节点是root 节点下第一个节点
		//修改根节点 指向转转后的节点
		rbt.Root = tmp
	}else if root.Parent.Left == root {
		//如果 旋转前的root节点是父节点的 左边节点
		//更新成旋转后的tmp节点
		root.Parent.Left = tmp
	}else if root.Parent.Right == root{
		//如果 旋转前的root节点是父节点的 右边节点
		//更新成旋转后的tmp节点
		root.Parent.Right = tmp
	}
	//把 root 的父节点 跟新 为tmp
	root.Parent = tmp
	//tmp 的左边节点更新成 root
	tmp.Left = root
}
右旋

在这里插入图片描述

//左旋转 和AVL树的旋转 是一样的 但是 我们旋转的同事还对parent 父节点
//的指针进行维护
func (rbt *RBtree) rbRightRotate(root *RBNode){
	//如果 根节点的左边为nil节点 那么就无法进行右旋 直接返回
	if root.Left == rbt.NIL{
		return
	}
	tmp := root.Left
	root.Left = tmp.Right
	//下面是调整旋转后的父节点
	if root.Left != rbt.NIL{
		//子节点的 父节点设置为root节点
		root.Left.Parent = root
	}
	//跟新 tmp的父节点 为 root 的父节点
	tmp.Parent = root.Parent
	if root.Parent == rbt.NIL{
		//如果 旋转前的root节点是root 节点下第一个节点
		//修改根节点 指向转转后的节点
		rbt.Root = tmp
	}else if root.Parent.Left == root{
		//如果 旋转前的root节点是父节点的 左边节点
		//更新成旋转后的tmp节点
		tmp.Parent.Left = tmp
	}else if root.Parent.Right == root{
		//如果 旋转前的root节点是父节点的 右边节点
		//更新成旋转后的tmp节点
		tmp.Parent.Right = tmp
	}
	//将 root 的父节点指向 它的 左节点
	root.Parent = tmp
	//tmp 的右边节点更新成 root
	tmp.Right = root
}

红黑插入及修复

红黑树的插入 还是比较简单的,主要是 判断叔叔节点的颜色 叔叔节点是 左边的情况 叔叔节点是右边的情况, 你在祖父节点的右边的右边 左边的左边这几种情况 分别 对应 处理.

红黑树 插入有 几种 情况需要处理

在这里插入图片描述

红黑树插入调整3大类父节点颜色叔叔节点为nil叔叔节点为红色
第一大类红色如果 插入节点在 祖父节点的右边的右边那么进行一次 左旋转操作 将祖父和父节点 进行一次变色第二大类处理↓
/红色如果 插入节点在 祖父节点的左边的左边那么进行一次 右旋转操作 将祖父和父节点变一次色第二大类处理↓
/红色如果 插入节点在 祖父节点的左边的右边那么进行一次 左右旋转操作 将自己和祖父变一次色第二大类处理↓
/红色如果 插入节点在 祖父节点的右边的左边那么进行一次 右左旋转操作 将自己和祖父变一次色第二大类处理↓
第二大类红色只需要将 祖父节点设置为红色 父亲,叔叔节点设置为黑色,将祖父作为新增节点递归处理←同左
第三大类黑色直接插入,不作处理直接插入,不作处理

红黑树,一共 3大类 需要处理,其中对节点的调整旋转 只有4种情况和AVL树及其相似,
好吧 就是一样的 但是有一点区别,因为 红黑树 第二大类 需要向上 递归处理节点颜色,所以需要 一个父节点指针。

在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
在这里插入图片描述
所以 红黑树 这12 种情况 就能解决插入所能遇到的所有情况,其实 它就是不断的 在个中插入状态之间调整 变换 其实很简单,如下图 :

在这里插入图片描述
插入 其实很简单 就是 左右 去比较大小,然后找到 nil 节点 赋值就好了,关键是 插入后 我们 还需要 按照上面的逻辑,对颜色 作修复处理,当然 如果插入的父节点是黑色那么什么都不用做了。

func (rbt *RBtree) Insert(item Data) *RBNode{
	//如果 插入 nil
	if item == nil{
		return nil
	}
	node := RBNode{}
	//默认插入为 红色
	node.Color = RED
	//数据赋值
	node.Data = item
	node.Right =rbt.NIL
	node.Left =rbt.NIL
	return rbt.insert(&node)

}
func (rbt *RBtree) insert(node *RBNode) *RBNode{
	ptr := rbt.Root
	nptr := rbt.NIL
	//找到要插入的位置
	for ptr != rbt.NIL{
		nptr = ptr
		if less(node.Data,ptr.Data){
			//插入的node 比 当前节点小 往左边寻找
			ptr = ptr.Left
		}else if less(ptr.Data,node.Data){
			//插入的node 比 当前节点大 往右边寻找
			ptr = ptr.Right
		}else{
			//如果 已经插入过了
			return ptr
		}
	}
	//跟新插入节点的父节点
	node.Parent = nptr
	//如果根节点 为nil
	if nptr == rbt.NIL{
		//往根节点插入
		rbt.Root = node
	}else if less(node.Data,nptr.Data){
		//比当前节点小 往左边插入
		nptr.Left = node
	}else{
		//比当前节点大 往右边插入
		nptr.Right = node
	}

	//修复插入
	rbt.insertFixUp(node)
	rbt.count ++
	//保证 根节点 是黑色的
	rbt.Root.Color = BLACK
	return node
}

好了 说了那么多 我们看看实现代码吧,其实只要照着上面图片描述的逻辑 其实很容易, 我们在判断的时候起点是 插入节点的 只要找到 父亲的父亲 的左边 或者右边 找到叔叔 然后再 作相应的处理。

//插入 后平衡 有3种情况 如果父节点是黑色 那么 不需要做任何 处理
//如果 父节点 是红色 分成 2类  取决于 叔叔的颜色 如果叔叔是黑色 是一类 如果叔叔是红色 又是一类
// 如果叔叔是红色 我们需要对 父节点 叔叔节点 进行变成黑色 祖父节点 变成 红色  然后把 祖父节点当成新节点 在重复 去判断这3类情况
//如果叔叔是黑色 我们需要 我们 要判断 祖父节点 和我们之间的 关系 分为4类 如果 我们在 祖父的左左 右右 左右 右左 处理方法分别是 右旋转 坐旋转 右左旋转 左右旋转
//四种情况 旋转后 如果是 左左 或者 右右 我们只需要 反转一次 父节点 和祖父节点的颜色 如果是 右左 或者 左右 旋转 那么 我们需要反转一次 自己 和祖父的颜色
func (rbt *RBtree) insertFixUp(node *RBNode){
	//通过循环从底向上修复 修改颜色导致的失衡直到根节点
	for  node != rbt.NIL && node.Parent.Color == RED {
		//如果父节点在祖父节点的左边
		if node.Parent == node.Parent.Parent.Left {
			//如果祖父节点 在父节点的 左边 那么叔叔 一定就在右边
			uncle := node.Parent.Parent.Right
			//如果叔叔节点颜色 是红色的
			//红色的情况下 我们只需要 染色处理 将父节点 叔叔节点染成 黑色 将祖父节点染成红色
			if uncle.Color == RED{
				//父节点 黑
				node.Parent.Color = BLACK
				//叔叔节点 黑
				uncle.Color = BLACK
				//祖父节点 红
				node.Parent.Parent.Color = RED
				//移到 祖父节点重新处理 这个过程
				node = node.Parent.Parent
			}else{ //黑色的我们需要通过旋转 保持黑平衡
						//如果 叔叔节点是 黑色的 或者NIL
							if node.Parent.Left == node {
								//如果 当前节点在父节点的左左 那么 就 只想一次 右旋 父节点和当前节点颜色翻转一下
								rbt.InverseColor(node.Parent)
								rbt.InverseColor(node.Parent.Parent)
								rbt.rbRightRotate(node.Parent.Parent)
								//移到 祖父节点重新处理 这个过程
								node = node.Parent.Parent
							} else {
								//如果 当前节点在父节点的左右 那么 对 父节点先 左旋一次 再对祖先 节点再右旋一次 然后将自己和祖父节点
								//进行一次颜色反转
								rbt.InverseColor(node)
								rbt.InverseColor(node.Parent.Parent)
								rbt.rbleftRotate(node.Parent)
								rbt.rbRightRotate(node.Parent.Parent)
								//移到 祖父节点重新处理 这个过程
								node = node.Parent.Parent
							}
			}
			}else{//这里是处理 uncle 在 自己的右侧 就是比自己大的情况
				//如果祖父节点 在父节点的 右边 那么叔叔 一定就在左边
				uncle := node.Parent.Parent.Left
					if uncle.Color == RED{
						//父节点 黑
						node.Parent.Color = BLACK
						//叔叔节点 黑
						uncle.Color = BLACK
						//祖父节点 红
						node.Parent.Parent.Color = RED
						//移到 祖父节点重新处理 这个过程
						node = node.Parent.Parent
					}else{//黑色的我们需要通过旋转 保持黑平衡
						//如果 叔叔节点是 黑色的 或者NIL
						if node.Parent.Right == node {
							//如果 当前节点在父节点的右右 那么 就 只想一次 左 父节点和祖父节点惊醒一次颜色反转
							rbt.InverseColor(node.Parent)
							rbt.InverseColor(node.Parent.Parent)
							rbt.rbleftRotate(node.Parent.Parent)
							//移到 祖父节点重新处理 这个过程
							node = node.Parent.Parent
						} else {
							//如果 当前节点在父节点的右左 那么 对 父节点先 右旋一次 再对祖先 节点再左旋一次 然后将自己和祖父节点进行一次颜色反转
							//进行一次颜色反转
							rbt.InverseColor(node)
							rbt.InverseColor(node.Parent.Parent)
							rbt.rbRightRotate(node.Parent)
							rbt.rbleftRotate(node.Parent.Parent)
							//移到 祖父节点重新处理 这个过程
							node = node.Parent.Parent
						}

					}
				}
		}
}

红黑树的删除及修复
红黑树的删除

首先 我们先不考虑 红黑树的特性 如果一颗二叉树要 删除 一个节点 一般 我们 只需要要在子树下节点找个节点代替被删除的节点,这样 删除的 其实是哪个被替换的节点。

我们看到如下图,红色节点是被删除的节点, 我们需要找到 它底下的右边的最小节点 的数据 替换它,我们看 第一种情况 替换前 a < b < c 替换后 b < f < c 的性质哦没有被打破。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面的删除 原理 其实和AVL树里面的原理 是一模一样的, 再删除完成后 我们 需要判断删除节点的颜色 是不是黑色,如果是黑色 需要做修复,至于为什么黑色需要做修复 其实很简单,红黑树是一颗 黑平衡的树 当你删除黑色的时候它 就 不平衡了,节点就会向下溢出,所以需要修复。

func (rbt *RBtree)delete(key *RBNode)*RBNode{
	//找到 要删除的元素
	DeletedElement := rbt.search(key)
	//如果没找到 不需要删除
	if DeletedElement == rbt.NIL{
		return rbt.NIL //找不到
	}
	//用做返回 被删除的元素
	ret := &RBNode{
		Left:   rbt.NIL,
		Right:  rbt.NIL,
		Parent: rbt.NIL,
		Color:  DeletedElement.Color,
		Data:   DeletedElement.Data,
	}
	//被删除的 节点 和 被删除节点下面的子节点
	var RealDeletedElement *RBNode
	var RealDeletedElementSub *RBNode
	// 当 树 下面 只有一个节点的情况下
	// 被删除元素         
  	if DeletedElement.Left == rbt.NIL || DeletedElement.Right == rbt.NIL{
		RealDeletedElement = DeletedElement
	}else{
		//如果 实际被删除节点 节点下面有2个元素 那么 不能直接删除 找到 
		//找到 他的 后继 代替它被删除
		RealDeletedElement = rbt.successor(DeletedElement)
	}
	//如果被删除节点的左边不为 nil的话
	if RealDeletedElement.Left != rbt.NIL{
		//把被删除节点的左边保存下
		RealDeletedElementSub = RealDeletedElement.Left
	}else{
		//否则 获取 被删除节点的右边 可能为 nil 可能不为 nil
		RealDeletedElementSub = RealDeletedElement.Right
	}
	
	//实际被删除节点的子节点的 父节点 设置为 被删除节点的父节点
	RealDeletedElementSub.Parent = RealDeletedElement.Parent

	//如果实际被删除节点的父节点 是根节点 那么 直接把子节点设置为根节点下面的节点
	if RealDeletedElement.Parent == rbt.NIL {
		rbt.Root = RealDeletedElementSub

	}else if RealDeletedElement == RealDeletedElement.Parent.Left{
		//如果 实际被删除的节点 是父节点左孩子
		//那么 把 实际被删除节点的 左孩子节点设置为 实际被删除节点的子节点
		RealDeletedElement.Parent.Left = RealDeletedElementSub
	}else{
		//如果 实际被删除节点的 是父节点的右孩子
		//那么 把实际被删除节点  右孩子 设置为 实际被删除节点的子节点
		RealDeletedElement.Parent.Right = RealDeletedElementSub
	}
	//如果实际被删除的节点 和 被删除节点 是不一样的
	// 那么这种情况 我们认为 是 左右 都不为 nil 的情况 我们通过 被删除节点的右边最小
	//的节点 代替了 被删除节点 进行 删除 所以 需要把数据交给被删除的节点,这样被删除节点不需要做什么调整
	//实际被删除节点会代替 它 被删除
	if RealDeletedElement != DeletedElement {
		DeletedElement.Data = RealDeletedElement.Data
	}
	//如果删除节点是黑色的情况下,我们需要对子节点进行修复
	//实际上所有节点删除基本上都发生在 叶子节点
	//如果 左右都不为nil的节点是 无法被实际删除的
	if RealDeletedElement.Color == BLACK && RealDeletedElementSub.Color == BLACK{
	 	rbt.deleteFixup1(RealDeletedElementSub)
	}else if RealDeletedElementSub.Color == RED{ //情况 0 如果 被删除节点下面 就有 红色节点 那么我们 直接 把它变黑
		RealDeletedElementSub.Color = BLACK
	}
	
	//对计数--
	rbt.count --
	//返回被删除的元素
	return ret
}
红黑树 删除及修复

上面 我们 分析了 红黑树 删除 需要找到 替代的子节点,但是没有提及颜色的操作。

红黑树的性质 规定了 黑色节点的高度是要一样的,从任何一点起途径的黑色节点数是一致的 但是没有限定红色的节点,所以 可以直接删除红色节点。

那么 删除 红色节点 只要找到 替代的节点也就是右边 最小 的叶子节点,所以 我们以下都讨论删除黑色的情况。

删除红色的节点删除黑色的节点 是孤儿删除的黑色节点左右至少有一个红色节点
红兄弟直接删除,找到替换节点让 兄弟下面的儿子 做一次旋转让 变成我的兄弟 转换成 黑兄弟 下面最后一种情况拿左或右的节点 替代 被删除的节点
黑兄弟 左右至少有一个红节点去找兄弟节点借( 通过旋转父节点)一个红色节点
黑兄弟 没有红色节点,父节点是红色将兄弟节点 染成 红色 将 父节点染成黑色
同上,但是父节点是黑色同上操作,不过同时需要将父节点 作为修复对象 递归调用修复
情况0 删除的黑色节点左右至少有一个红色节点

在这里插入图片描述
在下面所有图片里 我们 默认 粉红色的节点为被删除节点 而且是黑色节点,如果 它下边 至少有一个红色节点,我们直接 把它改成黑色 替代被删除节点即可,这是除了删除红色节点以外最简单的一种情况了。

情况1 删除节点的兄弟为黑色 且它底下至少有一个红色节点

这种情况下,我们试图 从 兄弟节点 借一个红色节点 用来代替 被删除黑色节点的位置,来保持红黑树的黑平衡,至于怎么借,我们通过旋转 来实现。
在这里插入图片描述
在这里插入图片描述

图上 介绍了 情况一,可能遇到的所有情况 但是实际上 只有 4种 需要判断。

兄弟节点在父节点左边兄弟节点在父节点右边
红色节点在兄弟节点的左边左左 => 父节点一次右旋右左 ->兄弟节点 一次右旋父节点再左旋
红色节点在兄弟节点的右边左右 =>兄弟节点一次左旋父节点再右旋右右 -> 父节点一次左旋

其实 和判断 红黑树的插入 AVL树的插入 的 左或右旋 或 左右 右左旋 都是一个原理。
这里再 体积一点,这里 我们 是以父节点是红色情况下分析的,但实际上 父节点 可能是
黑色的 但旋转后 效果是一样的,所以 这也是为什么 我们旋转后 要将新的 父节点 颜色 设置为 原来父节点的颜色。

下面是情况1 的 代码,可以对比着 图片看,就比较容易理解了。

					//情况1 处理方法 处理 兄弟节点 在父节点右边的情况 
					//如果 父节点的右边 的右边为黑色 处理右边的左边
					if brother.Right.Color == BLACK{
						//下面旋转后新的 父节点 和原来父节点颜色 一致
						brother.Left.Color = remove.Parent.Color
						//父节点 会变成了新的兄弟节点 改为黑色
						remove.Parent.Color = BLACK
						//对兄弟节点 进行一次 右旋
						rbt.rbRightRotate(brother)
						rbt.rbleftRotate(remove.Parent)
						//等价于 break
						remove = rbt.Root

					}else{//如果 父节点的 右边为 红色处理 方法
						//下面 左旋 一次后 父节点 右边的兄弟节点 会变成新的父节点
						//所以需要继承 原先父节点的颜色
						brother.Parent.Color = remove.Parent.Color
						//父节点 会变成兄弟节点的 左子节点 设置成黑色
						remove.Parent.Color = BLACK
						//兄弟节点下面的 右节点 变黑
						brother.Right.Color = BLACK
						//准备好上面的步骤 可以进行左旋
						rbt.rbleftRotate(remove.Parent)
						//等价于 break
						remove = rbt.Root
					}
					---------------------
					//情况1 处理方法  处理 兄弟节点 在父节点左边的情况 
					//如果 父节点的左边 的左边为黑色 处理左边的右边
					if brother.Left.Color == BLACK{
						//下面旋转后新的 父节点 和原来父节点颜色 一致
						brother.Right.Color = remove.Parent.Color
						//父节点 会变成了新的兄弟节点 改为黑色
						remove.Parent.Color = BLACK
						//对兄弟节点 进行一次 右旋
						rbt.rbleftRotate(brother)
						rbt.rbRightRotate(remove.Parent)
						remove = rbt.Root
					}else{//如果 父节点的 右边为 红色处理 方法
						brother.Parent.Color = remove.Parent.Color
						remove.Parent.Color = BLACK
						brother.Left.Color = BLACK
						rbt.rbRightRotate(remove.Parent)
						remove = rbt.Root
						}
					
情况2. 删除节点的兄弟为 黑色 但底下都为黑色

在这里插入图片描述
如果 兄弟节点底下 都为黑色,我们不需要旋转 操作,我们只需要 把 兄弟节点 的颜色 改为红色 这样相当于 把 另一边 的 黑高度 减去了 1,但是 此时 我们要分析 如果父节点
为 红色 那么 我们 把父节点 改为 黑色,这样就实现了 被删除的另一边 高度建议 同时 父节点 的高度 左右 都是共享的 这样 左右就能 保持平衡了,但是如果原先 父节点 就是了一个黑色,此时我们把黑兄弟改成红兄弟这样 本来 2个黑 变成了 1红 一黑 少了个黑 高度 肯定就发生变化了,怎么办呢 我们解决不了 这个问题 就把它往上 抛把 把父节点 当做被删除黑节点,再做一次删除的修复。 好在 只要我们维持了 红黑树的5条性质的话往上修复 最终 红黑树 会变成 一层黑 一层红的这样平衡状态。
上面 只提及了 被删除节点在右边的情况,在右边情况 也是一摸一样的操作。

我们 把上面图 上的内容 用代码 实现下吧:

				//如果 父节点是红色的情况下 我们 把父节点改为黑色
					if remove.Parent.Color == RED{
						//父节点 改为 黑色
						remove.Parent.Color = BLACK
						//循环条件结束 修复完成 如果改成 break 
						//那么需要把 brother.Color = RED 这句加上
						remove = rbt.Root
					}else{//父节点 为黑色的情况下
						//父节点 作为删除节点继续修复
						remove = remove.Parent

					}
					//兄弟节点 改为红色
					brother.Color = RED
				}
情况3. 删除节点的兄弟节点为红色

在这里插入图片描述
情况 3 我们 需要将其做一次 左旋或者右旋 如果 被删除节点在 左边 我们镜像一次 左旋,在右 就一次右旋,然后 就会变成 兄弟节点 为 黑色 的情况 也就是情况 2 我们再把 修复删除 这个 过程在执行一遍即可。

分析 到这里就结束了,讲了这么多 如果 不用代码实现一遍 就像纸上谈兵,没有什么意义 而且 没法验证 我上面写的 逻辑是否正确,为了方便大家学习 我们将用伪代码来实现。

情况 3 实现代码:

				//删除节点为 右边的情况下
				brother.Color = BLACK
				remove.Parent.Color = RED
				//一次左旋
				rbt.rbleftRotate(remove.Parent)
				
				//删除节点 为左边情况下
				brother.Color = BLACK
				remove.Parent.Color = RED
				//一次右旋
				rbt.rbRightRotate(remove.Parent)
				
编写删除修复代码

删除代码 上面已经实现了,相对来说还是比较简单的。删除代码的逻辑里 我们可以直接处理了情况0 ,也就是判断被删除节点的子节点 有没有红色,有就改成黑色。 就修复完成了。

首先 我们 上面 所有 判断 都是 依据兄弟节点的位置,来判断的。
并且 兄弟节点 可能在 父节点的左边 或者在父节点的右边 会有不同的情况。

所以我们先判断 兄弟节点位置:
在这里插入图片描述

//删除 修复
func (rbt *RBtree)deleteFixup(remove *RBNode)*RBNode{

	for remove != rbt.Root && remove.Color == BLACK{
		if remove.Parent.Left == remove{//删除节点在左边
			//兄弟节点在 右边
			brother := remove.Parent.Right
		}else{ //删除节点在右边
			//兄弟节点在 左边
			brother := remove.Parent.Left

		}
	}
	return nil
}

有了位置 我们需要判断 下一步 我们需要判断一下父节点的颜色,如果黑色对应情况 1 2 红色 对应 情况3,再把我们分析的 情况3代码 填上,分别是在左边或者在右边

	if brother.Color == RED{ //兄弟节点为红色
				
			}else{ //为黑色

			}

好了 情况 1 2 下面 再 细分 如果 兄弟节点 左右下面 都是黑色 那么就是情况2

	if brother.Left.Color == BLACK && brother.Right.Color == BLACK{

				}
	

下面 我还需要 处理情况 1.,就是判断下 兄弟节点的 红色节点在左在右,来进行左旋或右旋 或左右旋操,情况 1 我们 和 情况 2 是有可能 同时 满足的 但是它们 之间 不能同时
处理 所以 接在 情况 2 else 后面

if brother.Left.Color == BLACK && brother.Right.Color == BLACK{

				}else{
				//情况1 处理方法
				//如果 父节点的右边 的右边为黑色 处理右边的左边
				if brother.Right.Color == BLACK{

				}else{//如果 父节点的 右边为 红色处理 方法


				}
}

上面 我们搭建了一个基本的处理框架,只要把 上面3种情况的代码填 空一样填进去就好了。

//删除 修复
func (rbt *RBtree)deleteFixup(remove *RBNode)*RBNode{
	for remove != rbt.Root && remove.Color == BLACK{
		if remove.Parent.Left == remove{//删除节点在左边
			//兄弟节点在 右边
			brother := remove.Parent.Right

			if brother.Color == RED{ //兄弟节点为红色 情况3
					//情况 3 删除节点在左边

			}else{ //为黑色 情况 1 2
				if brother.Left.Color == BLACK && brother.Right.Color == BLACK{ //情况 2 处理

				}else{

				//情况1 处理方法
				//如果 父节点的右边 的右边为黑色 处理右边的左边
				if brother.Right.Color == BLACK{

				}else{//如果 父节点的 右边为 红色处理 方法


					}
				}

	
			}

		}else{ //删除节点在右边
			//兄弟节点在 左边
			brother := remove.Parent.Left
			if brother.Color == RED{//兄弟节点为红色
				//情况 3 删除节点在右边
			}else{//为黑色
				if brother.Left.Color == BLACK && brother.Right.Color == BLACK{//情况 2处理

				}else{//处理情况1 
				//情况1 处理方法
				//如果 父节点的左边 的左边为黑色 处理左边的右边
				if brother.Left.Color == BLACK{

				}else{//如果 父节点的 右边为 红色处理 方法


					}
				}
				

			}

		}
	}
	return nil
}

好了,上面 就是红黑树 实现的 增删,了 其他改是很简单的查 完全可以套用普通二叉树的模板。

下面 我们 对红黑树的,顺序插入 1千万 数据

在这里插入图片描述
树的高度为 45 插入花费 4秒 效率还是挺高的.
完整代码见 github:https://github.com/qiaojinxia/AlgorithmCode/tree/master/RBTree

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值