红黑树(Red Black Tree)是一种自平衡的二叉搜索树(Self-balancing Binary Search Tree)。以前也叫做平衡二叉 B 树(Symmetric Binary B-tree)。
预备知识
平衡二叉搜索树
平衡二叉搜索树(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:替换结点的兄弟结点的子结点都为黑色
- 如果父结点为黑色:将兄弟结点变为红色;将父结点作为新的替换结点;重新进行删除结点的场景处理。
- 如果父结点为红色:替换结点的父结点和替换结点的兄弟结点颜色交换;删除结点和替换结点的值交换后,删除替换结点。