序言

在讲红黑树之前,我得先讲一段废话,因为这有利于我们理解红黑树!

大家都玩过猜数字的游戏吧?
这个游戏规则很简单:给定一个范围,让我们猜这个数。如果我们猜错,游戏会根据我们猜的数提示猜是大了还猜是小了,然后我们根据提示再重新再猜。
现在我们再来重玩下这个游戏:

猜一下一个 1 - 100 的数:
比如说一个数:69
我们输入:50
游戏提示:小了
继续输入:75
游戏提示:大了
继续输入:63
游戏提示:小了
继续输入:69
游戏提示:恭喜您!4 次便完成这个游戏。

100 个数字,仅用 4 次我们就猜中了,除了感叹我们人类的聪明以外,还得感谢一个算法:二分查找法

1、二分查找法

二分查找法的原理,我就不赘述了,大家都会(不会的也不必往下看了,真的很难看懂。手动狗头.jpg)。

序言中的猜数字游戏就是一个简单的二分法应用。

下面我们再来一个二分法的应用吧!

请在有序序列 [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] 中找到 17 的位置。

1、取序列中心元素(11)与 17 进行对比,11 < 17
2、取序列后半中心元素(15)与 17 进行对比,15 < 17
3、去序列后半的后半中心元素(17)与 17 进行对比,17 == 17
4、结束,返回当前位置

以上为有序序列中查找某个数位置,那如果想在无序列表中找某个数所在的位置,用二分法可以吗?答案显然是不可以的。

假如想在无序列表 [1, 3, 17, 5, 7, 9, 11, 13, 15, 19] 中找到数字 17 所在的位置。我们尝试使用二分法去查找,显然是不可能得到我们想要的结果。因为我们无法保证中间位置数 9 前面的数均小于 17 ,所以无法得到正确结果。

此时我们只能通过遍历来一个个进行匹配,此时的时间复杂度为 $O(n)$。假如查找的序列元素个数为 $10^9$,我们最多需要进行 $10^9$ 次查找,而如果我们使用二分查找法,最多需要 30 次,便能找到我们想要的那个数的位置。

由此可见二分查找法的重要性。

所以我们最好得使用二分法来进行查找,而二分法的前提是序列有序。

所以我们需要对待查找的序列进行排序,而排序的时间复杂度一般是 $O(nlog(n))$。

那么除了排序以外,我们就没有其他办法了吗?

当然有,我们还可以构建二叉搜索树来解决这个问题。

那构建二叉搜索树和排序比,有什么优劣呢?

请接着往下看……

2、二叉搜索树

二叉搜索树其核心思想也是利用二分法进行:

二叉搜索树定义如下:
左子树值 < 根节点值 < 右子树值

root 为当前查找的节点
则二叉搜索树的查找过程如下:

1、待查找关键字与 root 进行比较
2、如果小于 root,再将其与左子树比较;
3、此时,root = root.左子树,回到步骤 1;
4、如果大于 root,则将再其与右子树比较;
5、此时,root = root.右子树,回到步骤 1;
6、如果根节点值等于查找值,结束;

image.png

构建

二叉搜索树的构建过程如下:
root 为待构建的二叉搜索树的根节点

1、选取待插入二叉搜索树的值;
2、如果 root 为空,直接插入并设置为根节点,结束;
3、如果 root 小于待插入的值,root = root.左子树,回到 2;
4、如果 root 大于待插入的值,root = root.右子树,回到 2;
5、如果 root 等于待插入的值,结束;

image.png

构建二叉搜索树的时间复杂度在最好情况下为 $O(nlog(n))$,最坏情况下为 $O(n^2)$ 和排序算法时间复杂度差不多。

既然差不多,那我们还要费劲巴拉的去构建一棵二叉搜索树干嘛?排序他不香吗?

排序香是香,但是他有一定的局限性:

  • 1、我们能直接通过下标获取序列中的元素的前提是序列中元素所占内存连续;
    c 语言中可以通过提前分配或者 malloc 函数获取。前者内存大小不可更改,编译时就已经确定内存大小;后者只能保证该次申请的内存连续,即第 n 次利用 malloc 申请的内存和第 n + 1 次申请的内存并不一定连续。在 JavaC++ 容器对象中,虽然可以动态增长连续内存,但如果遇到内存碎片化的问题,会更糟糕。除此之外它也会遇到下面的问题。

image.png

  • 2、每次插入、删除新元素的代价太大;
    删除元素(如果不是最后一个),会造成元素内存不连续。如图 2-3 我们删除第 2 个元素(20),再次访问 sequence[2] 时希望得到 28 ,此时我们得到的还是内存 0xabcd08 中的值(如果没有更改)20。这显然不是我们所期待的结果,为了解决这个问题,我们需要将第 2 个以后的所有元素,往前移一位。
    增加元素(假设内存够用),会引起内存覆盖问题。还是图 2-3 ,我们往第 0 个元素插入 11 ,然后再次访问 sequence[1] 时,我们希望得到 13 但实际上得到的结果却是 18 。所以我们需要将第 0 个及其以后的元素全部后移一位。

综上所述,我们还是老老实实的构建二叉搜索树吧!

我们尝试根据图 2-2 流程,分别对序列 [40, 20, 10, 60, 50, 70][10, 20, 40, 50, 60, 70][70, 60, 50, 40, 20, 10] 构建二叉搜索树,得到如下结果:
image.png

诶(此处发第二声),等等,除了图 2-4 以外,其他画风有点不对啊(六星连珠啊)。我们细看一下,发现这三个序列中的元素都一样,只不过位置不一样而已,然后构建出的二叉搜索树差距如此之大???(?一种米还养百种人呢)

像图 2-4 的二叉搜索树,我们查找的时间复杂度平均为 $(O(logN))$,而图 2-52-6 的查找平均时间复杂度为 $O(N)$(此处杠 $N/2$ 的可以去看看时间复杂度和 $O$ 的定义)。

那这不扯犊子吗?手机性能再好,品控做不好,谁敢买啊?(?意有所指)

3、平衡二叉树

老祖宗有句话叫:无规矩、不成方圆。

为了避免出现图 2-52-6 的情况发生。计算机先驱们又定义了一个平衡二叉树,它较二叉树仅多了一条规定:左右子树的高度之差,不能超过 1。所以平衡二叉树里面会多一个值域,叫 平衡因子,它的值只可能为 1, 0, -1 。当平衡二叉树出现失衡时(左右子树高度相差大于 1 ),需要作出相对应的调整。

调整有四种,分别为:LL、LR、RL、RR。
其对应内容为:左子树的左子树引起的失衡、左子树的右子树引起的失衡、右子树的左子树引起的失衡、右子树的右子树引起的失衡。(有点绕,还是看图吧,节点头部数字为平衡因子)
image.png
image.png
image.png
image.png

看完上面的图后,会不会有种错觉,其实平衡二叉树也没那么难吗?

图羊,图什么(too yang, too simple

上面的只是理想状况下的失衡,社会还是很复杂的。

image.png
image.png
image.png
image.png

看完之后,先别急着崩溃。以上仅仅是插入失衡后调整,后面还有删除后失衡调整。看不懂也没关系,因为我今天要讲的是 红黑树 所以大家别急着放弃,这些与我们无关,只需要做个大致了解就好。

image.png

4、红黑树

终于讲到红黑树了?。

为什么会有红黑树的存在?

正如我们上面看到的平衡二叉搜索树(平衡二叉树和二叉搜索树的结合),在平衡二叉树上插入新的节点或者删除节点失衡后,需要进行调整,使其继续维持平衡。而这调整的代价太大,需要做的操作比较多,所以某位大佬就想办法搞了棵红黑树出来。

在红黑树中,并没有规定左右子树的高度相差不超过 1,所以它不是平衡二叉树

少了这条规定之后,是不是很开心?终于可以不用管那些劳什子 LLLRRLRR 调整了。

别高兴太早,我们还是先看看红黑树的定义吧!

1、树的节点不是红色就是黑色;(?,顾名思义也知道,easy)
2、根节点是黑色;(这也不难,easy)
3、叶子节点为黑色;(这里的叶子节点是指(nil),而不是没有子树的节点,不懂一会看图)
4、红色的节点子节点一定是黑色;(?好办,easy)
5、任一节点到其叶子节点所经过得黑色节点数量一致;(o((⊙﹏⊙))o 开始懵逼了)
6、……(没有啦)

好像上面没有讲到,哪些节点该为红色啊???(这就是我刚学的时候,懵逼的地方)
待插入的节点,刚开始都是红色的!!!(记住这个就好了)

image.png
image.png
image.png
image.png

除了最后一个,上面的插入看起来是不是很简单?确实很简单。

4.1 插入

那让我们来好好总结一下红黑树的插入规则吧!

1、待插入的节点始终为红色;
2、如果根节点为空,直接将待插入的节点置为根节点,将颜色置为黑色,结束;
3、如果节点存在,则结束插入操作;
4、具体插入位置查找可以参考前文的二叉搜索树的构建;
5、如果待插入的父节点为黑色,直接插入,结束;


插一下提示:
以下父节点为红色,如果为黑色第 5 步就结束了


6、如果待插入的叔父节点为红色;
6.1、父节点和叔父节点置为黑色,祖父节点置为红色;
6.2、如果祖父节点为根节点,再将其置为黑色;
6.3、结束;

再插一段提示:
由性质5(任一节点到其叶子节点所经过得黑色节点数量一致)可知,如果父节点为红色,叔父节点为黑色,则叔父节点一定为 nil,不理解可以再看看上面的图

7、如果父节点为祖父节点的左子树;
7.1、如果插入节点为父节点的左子树(具体如下图);

image.png

7.1.1、父节点置为黑色,祖父节点置为红色,然后对祖父节点进行右旋,结束;(具体如下图)

image.png

7.2 插入节点为父节点的右子树;

7.2.1 对父节点进行左旋(具体如下图);

image.png
是不是和 7.1 相似???那就回到 7.1.1 吧;


第 7 步的操作均为父节点为祖父节点的左子树,接下来的第 8 步均为父节点为祖父节点的右子树;


8、父节点为祖父节点的右子树;
8.1、如果插入节点为父节点的右子树(具体如下图);

image.png

8.1.1、父节点置为黑色,祖父节点置为红色,然后对祖父节点进行左旋(具体如下图),结束;

image.png

8.2、如果插入节点为父节点的左子树(具体如下图);

image.png

8.2.1、对父节点进行右旋操作;

image.png

是不是和 8.1 相似???那就回到 8.1.1 吧;

如果对左旋右旋操作难以理解,可以简单想象为逆时针顺时针旋转


再插一句,回过头去看看平衡二叉树的理想状态下的失衡,他们的情况是否很相似???这就是红黑树的整体性能优于平衡二叉输的原因了!


下图为红黑树的插入调整逻辑流程图:
image.png

4.2 删除

好了红黑树的插入部分讲完了,接下来将删除部分咯!!!

删除部分要比插入部分难的多哟!做好心理准备了吗???

image.png

以上红黑树,删除根节点后,如何调整???

未完、待续……


老鸨是个胖子
1 声望0 粉丝

苕之华,其叶青青;知我如此,不如无生。


引用和评论

0 条评论