面试被问“红黑树”,我一脸懵逼......
红黑树(Red Black Tree)是一种自平衡的二叉搜索树(Self-balancing Binary Search Tree)。以前也叫做平衡二叉 B 树(Symmetric Binary B-tree)。
图片来自 Pexels
预备知识
平衡二叉搜索树
平衡二叉搜索树(Balanced Binary Search Tree),英文简称 BBST。经典常见的平衡二叉搜索树是 AVL 树和红黑树。
①二叉搜索树
二叉搜索树(Binary Search Tree)是二叉树的一种,英文简称 BST。又称为二叉查找树、二叉排序树。
它的特点是任何一个结点的值都大于其左子树的所有结点的值,任何一个结点的值都小于其右子树的所有结点的值。
②平衡
一棵二叉搜索树平均时间复杂度可以认为是树的高度 O(h)。像左边这棵,结点的左右子树的高度接近,属于一棵平衡二叉搜索树,O(h) = O(logn);而右边这棵,高度达到了最大,已经退化成了链表,O(h)=O(n)。
③改进二叉搜索树
当二叉树退化成链表时,性能是很低的,所以我们需要在结点的插入、删除操作之后,想办法让二叉搜索树恢复平衡(减小树的高度)。
但是如果为了追求最理想的平衡,而增加了时间复杂度也不是很有必要,因此比较合理的方案就是:用尽量少的调整次数达到适度平衡。
由此引申出 AVL 树的概念。
AVL 树
AVL 树是最早发明的自平衡二叉搜索树之一,它取名自两位发明家的名字:G.M.Adelson-Velsky 和 E.M.Landis。
平衡因子(Balance Factor):某结点的左右子树的高度差。
举例:8 的左子树高度为 2,右子树高度为 1,因此它的平衡因子为 1;5 的左子树高度为 0,右子树高度为 3,因此它的平衡因子为 -3;4 的左子树高度为 2,右子树高度为 4,因此它的平衡因子为 -2;
可以看到 AVL 树具有以下特点:
每个结点的平衡因子只可能是 -1、0、1(如果绝对值超过 1,则认为是失衡)
每个结点的左右子树高度差不超过 1
搜索、插入、删除的时间复杂度是 O(logn)
B 树
这是一个简单的 3 阶 B 树:
特点如下:
1 个结点可以存储超过 2 个元素,可以拥有超过 2 个子结点
拥有二叉搜索树的一些性质
平衡,每个结点的所有子树高度一致
比较矮
①m 阶 B 树的性质(m ≥ 2)
根结点:1 ≤ x ≤ m - 1
非根结点:┌ m / 2 ┐ - 1 ≤ x ≤ m - 1
根结点:2 ≤ y ≤ m
非根结点:┌ m / 2 ┐ ≤ y ≤ m
②B 树 VS 二叉搜索树
B 树和二叉搜索树,在逻辑上是等价的
多代结点合并,可以获得一个超级结点,且 n 代合并的超级结点,最多拥有 (2^n) 个子结点 (至少是 (2^n) 阶 B 树)
红黑树定义和性质
每个结点是要么是红色或黑色
根结点必须是黑色
叶结点(外部结点、空结点)是黑色
红色结点不能连续(也就是,红色结点的孩子和父亲都是黑色)
对于每个结点,从该点至 nil(树尾端,Java 中为 null 的结点)的任何路径都包含所相同个数的黑色结点
红黑树与 B 树的等价变换
根据上面的性质,可以画出这样一棵红黑树。接下来对红黑树做等价变换,即将所有的红色结点上升一层与它的父结点放在同一行,这就很像一棵 4 阶 B 树,转换效果如下图所示。
红黑树与 4 阶 B 树(2-3-4树)具有等价性
黑色结点与红色子结点融合在一起,形成 1 个 B 树结点
红黑树的黑色结点个数与 4 阶 B 树的结点总个数相等
红黑树的基本操作
旋转操作是局部的。当一侧子树的结点少了,向另一侧“借”一些结点;当一侧子树的结点多了,则“租”一些结点给另一侧。
N-node:当前结点
P-parent:父结点
S-sibling:兄弟结点
U-uncle:叔父结点(P 的兄弟结点)
G-grand:祖父结点(P 的父结点)
左旋
左旋指的是以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
右旋
右旋指的是以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
变色
变换规则
把父结点和叔父结点变为黑色
把祖父结点变为红色
把指针定义到祖父结点
把父结点变为黑色
把祖父结点变为红色
对祖父结点右旋
红黑树搜索
由于红黑树本来就是平衡二叉搜索树,并且搜索也不会破坏树的平衡,所以搜索算法也与平衡二叉搜索树一致:
从根结点开始检索,把根结点设置为当前结点。
若当前结点为空,返回 nil。
若当前结点不为空,比较当前结点 key 与搜索 key 的大小。
若当前结点 key 等于搜索 key,那么该 key 就是搜索目标,返回当前结点。
若当前结点 key 大于搜索 key,把当前结点的左子结点设置为当前结点,重复步骤 2。
若当前结点 key 小于搜索 key,把当前结点的右子结点设置为当前结点,重复步骤 2。
红黑树插入
定位插入的位置
从根结点开始检索。
若根结点为空,那么插入结点设为根结点,结束。
若根结点不为空,那么把根结点设为当前结点。
若当前结点为 nil,返回当前结点的父结点,结束。
若当前结点 key 等于搜索 key,那么该 key 所在结点就是插入结点,更新结点的值,结束。
若当前结点 key 大于搜索 key,把当前结点的左子结点设置为当前结点,重复步骤 4。
若当前结点 key 小于搜索 key,把当前结点的右子结点设置为当前结点,重复步骤 4。
插入后实现自平衡
总结一下红黑树插入可能出现的所有场景:
将插入结点设为将要替换结点的颜色
更新当前结点的值为插入结点的值
由红黑树性质 4 可知:红色结点不能连续。那么此时该插入子树的红黑层数的情况是:黑-红-红。显然最简单的处理方式就是将其改为:红-黑-红。
将父结点和叔父结点变为黑色
将祖父结点变为红色
将祖父结点设置为当前插入结点
场景 4.2.1:插入结点是左子树
将父结点变为黑色
将祖父结点变为红色
将祖父结点右旋
这种场景显然可以转换为 4.2.1。
将父结点进行左旋
将父结点设为插入结点,得到场景 4.2.1
进行场景 4.2.1 的处理
场景 4.3.1:插入结点是左子树
将父结点变为黑色
将祖父结点变为红色
对祖父结点进行左旋
场景 4.3.2:插入结点是右子树
将父结点进行右旋
将父结点设置为插入结点,得到场景 4.3.1
进行场景 4.3.1 的处理
下面举个例子,往一棵红黑树中插入元素,整棵树的变换如下图所示:
红黑树删除
定位删除的位置
删除后实现自平衡
若删除结点无子结点,直接删除即可。
若删除结点只有一个子结点,用子结点替换删除结点。
若删除结点有两个子结点,用**后继结点(大于删除结点的最小结点)**替换删除结点。
具体应用,可以借助这张图理解:
在场景三情况下:删除结点用后继结点,如果后继结点有右子结点,那么相当于转换为场景二,否则转为场景一。
R:替换结点
P:替换结点的父结点
S:替换结点的兄弟结点
SL:兄弟结点的左子结点
SR:兄弟结点的右子结点
灰色:结点颜色可能是红色,也可能是黑色
若兄弟结点是红结点,那么根据红黑树性质 4,兄弟结点的父结点和子结点肯定为黑色,按照下图方式处理,得到删除场景 2.1.2.3。
将兄弟结点变为黑色
将父结点变为红色
对父结点进行左旋,得到场景 2.1.2.3
进行场景 2.1.2.3 的处理
如图所示:
将兄弟结点的颜色变为父结点的颜色
将父结点变为黑色
将兄弟结点的右子结点变为黑色
对父结点进行左旋
如图所示:
将兄弟结点变为红色
将兄弟结点的左子结点变为黑色
对兄弟结点进行右旋,得到场景 2.1.2.1
进行场景 2.1.2.1 的处理
兄弟子树没有红结点可以“借”了,再向父结点“借”。如果父结点是黑色,为了让父结点在所在的子树中保证平衡(替换结点即将删除,少了一个黑色结点,子树也需要少一个)先把兄弟结点变为红色,再让父结点成为新的替换结点。
如果父结点为黑色:将兄弟结点变为红色;将父结点作为新的替换结点;重新进行删除结点的场景处理。
如果父结点为红色:替换结点的父结点和替换结点的兄弟结点颜色交换;删除结点和替换结点的值交换后,删除替换结点。
场景 2.2.1:替换结点的兄弟结点为红色
将兄弟结点变为黑色
将父结点变为红色
对父结点进行右旋,得到场景 2.2.2.3
进行场景 2.2.2.3 的处理
将兄弟结点的颜色变为父结点的颜色
将父结点变为黑色
将兄弟结点的左子结点变为黑色
对父结点进行右旋
场景 2.2.2.2:替换结点的兄弟结点的左子结点为黑色,右子结点为红色
将兄弟结点变为红色
将兄弟结点的右子结点设为黑色
对兄弟结点进行左旋,得到场景 2.2.2.1
进行场景 2.2.2.1 的处理
场景 2.2.2.3:替换结点的兄弟结点的子结点都为黑色
如果父结点为黑色:将兄弟结点变为红色;将父结点作为新的替换结点;重新进行删除结点的场景处理。
如果父结点为红色:替换结点的父结点和替换结点的兄弟结点颜色交换;删除结点和替换结点的值交换后,删除替换结点。
编辑:陶家龙
出处:https://juejin.im/post/5e509b27f265da57455b3f33
精彩文章推荐: