数据结构 - 学习笔记 - 红黑树
- 定义
- 简介
- 知识点
- 1. 结点属性
- 2. 前驱、后继
- 3. 旋转
- 查找
- 插入
- 父结点为黑色
- 父结点为红色
- 1. 有4种情形只需要变色(对应234树4结点)
- 1.1. 变色实现平衡
- 1.2. 递归调整颜色
- 2. 有4种情形需要旋转 + 变色(对应234树3结点)
- 删除
- 无子结点,**黑色**,视兄弟颜色处理
- 1. 兄弟为红色
- 1.1. 找真兄弟(转换的另一种说法)
- 2. 兄弟为**黑色**
- 2.1. 兄弟有红色子节点
- 2.2. 兄弟无红色子节点
- 2.2.1. 父结点为红色
- 2.2.2. 父结点为**黑色**
- 时间复杂度
- 辅助脚本
- 参考资料
定义
- 所有结点非 红 即 黑 。(插入新结点默认红色,然后再按需调整)
根结点
必需是 黑 色。叶结点
必需是 黑 色。(叶结点是指最末端的空结点。通常在代码中直接用 null 表示,不会创建实际结点。)- 红 结点的
子结点
必需是 黑。有两棵
或无
子树。(不能有连续的两个红结点) 任意结点
到叶节点
经过的 黑 结点数量相同
。(别忘记叶结点
也是黑色结点 )
简介
红黑树
是234树
的一种实现,所以学习红黑树前,先看 红黑树前传——234树。- 它是一种
自平衡
的二叉查找树
。
2.1.二叉查找树
的左子树
所有结点值都<=
父,右子树
所有结点值都>=
父。
2.2.左右
子树也都是二叉查找树
。
2.3.二叉查找树
存在不平衡的情况,极端情况就是个链表
。 - 与
AVL
(平衡二叉查找树)相比,适当舍弃平衡,换取插入
、删除
性能提升的同时,兼顾了查找
效率。(本质上就是引入了红色结点的概念,它作为填充物,不计入树高,可有可无。因此简少了调平衡的工作量。) - 因此红黑树中保障的是 黑 结点的平衡,红 结点作为维持平衡的填充物不影响平衡。
- 最
短
路径可以全是 黑 结点。如: 黑→黑→黑 - 最
长
路径可以两 黑 夹一 红。如: 黑→红→黑→红→黑 - 当
插入
、删除
打破平衡时,通过变色
和旋转
实现再平衡。
知识点
1. 结点属性
颜色、键、左子结点、右子结点、父结点(特殊情况:根
结点没有父
结点)
2. 前驱、后继
- 前驱结点:从
左
子树中找最大
的结点 - 后继结点:从
右
子树中找最小
的结点
如果我们中序遍历一次,得到从小到大排列的所有元素,就能很直观的看出,前驱
和后继
就是当前结点身边一前一后的两位,删除当前结点,用它们其中一个补位,是最合适的了。
中序遍历结果:[2, 3, 4, 5, 6, 7, 8]
当前结点:5
前驱:4 后继:6
3. 旋转
- 旋转操作不会改变树中序遍历的顺序。
- 旋转操作通过
挪东墙补西墙
来维护二叉树的平衡。
(对E 进行)左旋 | (对S 进行)右旋 |
---|---|
- 总结一下几个结点的规律:
结点 | 缩写 | 左旋规律 | 右旋规律 |
---|---|---|---|
E | E | E.parent 更新为S E.right 更新为S.left | E.right 更新为 S E.parent 更新为 S.parent |
S | S | S.left 更新为 E S.parent 更新为 E.parent | S.parent 更新为E S.left 更新为E.right |
between E and S | BES | BES.parent 更新为 E | BES.parent 更新为 S |
less than E | LE | 不受任何影响 | 不受任何影响 |
greater than S | GS | 不受任何影响 | 不受任何影响 |
当前子树的父结点 | P | 如果当前是左子树,更新 P.left | 如果当前是右子树,更新 P.right |
- 伪代码
// 左旋
S = E.right;
BES = S.left;
E.right = BES;
BES.parent = E;
S.parent = E.parent; // E.parent == S.parent.parent
if(S.parent == null){ // S没爹,自己就是根
S.颜色 = 黑色
}else if(E.parent.left == E){ // E 是父的左子
E.parent.left = S;
}else{ // E 是父的右子
E.parent.right = S;
}
S.left = E;
E.parent = S;
查找
- 从根结点开始。
- 对比结点与查找目标,
2.1. 目标值=
当前结点:成功找到目标。
2.2. 目标值<
当前结点:递归查找左子树。
2.3. 目标值>
当前结点:递归查找右子树。
插入
红黑树插入共12
种情况:父黑4种,父红8种。
- 插入的新结点,默认都是红色结点。
- 插入位置的父结点为黑色。不影响平衡,共
4
种情况。直接插入即可。
2.1. 父结点无子结点,插入左子
结点。
2.2. 父结点无子结点,插入右子
结点。
2.3. 父结点有红色右子,插入左子
。
2.4. 父结点有红色左子,插入右子
。 - 插入位置的父结点为红色。
3.1. 对应234树
的3结点
有4种: LL、LR、RR、RL
3.2. 对应234树
的4结点
有4种:祖父黑色,父叔伯都是红色。
父结点为黑色
直接插入即可。
父结点为红色
1. 有4种情形只需要变色(对应234树4结点)
结点情况:父结点为红色且有红色叔伯结点。
1.1. 变色实现平衡
- 【红黑树】 插入新结点
- 【红黑树】
父
结点、叔伯
结点变成黑色,祖父
结点变成红色。
2.1.父
结点与祖父
结点调换颜色:满足红结点子必黑的定义。同时对于插入新结点的这一路径来说黑结点数未发生变化。
2.2.叔伯
结点变成黑色:祖父
结点原本作为公共的黑结点,挪给左路后,右路就少了一个黑结点。因此叔伯
要站出来变黑维持平衡。 - 【红黑树】 最后
祖父
结点更新为当前结点。
3.1. 判断曾祖
是否为红色。如果是,则需要向上递归调整颜色,一直到根。
3.2. 如果是根,直接染黑。
1.2. 递归调整颜色
插入新结点 1
后递归触发变色。
触发递归的原因:曾祖结点是染红,祖父结点染红后出现两个连续红色结点的情况,需要以祖父为当前结点继续进行变色处理。最终到达根结点
直接染黑结束。
2. 有4种情形需要旋转 + 变色(对应234树3结点)
结点情况:父结点为红色且无红色叔伯结点。
虽然共有4种
情形,但其中 LR
可以转为LL
, RL
可以转为RR
。总之就是有拐弯的,都是转为一条直线,再处理。
1.1. LR
左旋1次,转为 LL
。
1.2. LL
右旋1次 +
插入结点染黑、祖父结点染红,实现平衡。
2.1. RL
右旋1次,转为 LL
。
2.2. RR
左旋1次 +
插入结点染黑、祖父结点染红,实现平衡。
删除
删除可能发生在树中的任意位置。结点删除后,可能影响树的平衡,需要重新调整实现平衡。
根据将被删除的目标结点
的颜色
和子结点数量
,需要分别处理:
- | 当前红色 | 当前黑色 |
---|---|---|
有两个子结点 | 同黑色 | 1. 使用前驱 或后继 结点内容 替换当前结点。(颜色保持不变)2. 再删除 前驱 或后继 结点。(转变成了对:无子结点的删除) |
有一个子结点 | 直接删除 | 1. 删除当前结点。 2. 使用 子结点 填补当前位置,并将颜色设置为黑色。 |
无子结点 | 直接删除 | 1. 当前为根:直接删除。 2. 兄弟为红色:转为黑色再继续处理。 3. 兄弟为黑色:按兄弟有无红色子节点,分别处理。详情如下: |
无子结点,黑色,视兄弟颜色处理
1. 兄弟为红色
兄弟为红色:兄弟转为黑色再继续处理。转换方法:
- 将
兄弟
结点调整为黑色 - 将
父
结点调整为红色。 - 按
当前
结点的父
结点,进行右旋
。
1.1. 找真兄弟(转换的另一种说法)
红色兄弟转黑兄弟。也可以理解为:只有黑色才是真兄弟,红色是塑料兄弟。所以我们要找到真兄弟再干活。
- 当前是
左
子结点,就获取父亲
的右
子结点,判断颜色。如果是黑色,找到兄弟,完成任务。 - 结果发现是红色,
父
与兄
交换颜色。(不存在连续红,所以父必然是个黑结点) - 以
父结点
为支点,左旋
一次。 - 现在取
父结点
的右子结点
,就是真兄弟
了。(就是原
来兄弟
的左子
结点)
如果当前是右
子。同理往左边找就行了。
2. 兄弟为黑色
黑色是真兄弟,可算见到亲人了。可以开始真正的删除操作了。
2.1. 兄弟有红色子节点
借用兄弟子结点修复平衡。
2.2. 兄弟无红色子节点
借助父结点来修复平衡
2.2.1. 父结点为红色
- 删除当前结点。
兄弟
结点调整为红色。父
结点调整为黑色。
2.2.2. 父结点为黑色
- 删除当前结点。
兄弟
结点调整为红色。- 将
父
结点作为当前
结点,继续处理:
3.1.兄弟
调整为红色。
3.2. 如果父
为红色把父
结点染黑。否则递归重复第3步。
时间复杂度
操作 | 复杂度 |
---|---|
查找 | O(lgn) |
插入 | O(lgn) |
删除 | O(lgn) |
辅助脚本
红黑树可视化演示
var sleep = (delaytime = 1000) => {
return new Promise(resolve => setTimeout(resolve, delaytime))
}
async function delayDo(arr, callback = data=>console.log(`数据:${data}`), delaytime) {
var len = arr.length;
for (let i = 0; i <len ; i++) {
await sleep(delaytime);
callback(arr[i]);
}c
};
// 获取文本框
var [insertTxt, deleteTxt, findTxt] = [...document.querySelectorAll("#AlgorithmSpecificControls [type=Text]")];
// 获取按钮
var [insertBtn, deleteBtn, findBtn] = [...document.querySelectorAll("#AlgorithmSpecificControls [type=Button]")];
var process = {
insert: function insert(v){ insertTxt.value = v; insertBtn.click(); },
del: function del(v){ deleteTxt.value = v; deleteBtn.click(); },
find: function find(v){ findTxt.value = v; findBtn.click(); }
}
// 遍历数组,间隔 n 秒处理一个元素。
function main(arr = [...Array(10).keys()], cb = v=>console.log(v), delaytime=200){
delaytime = delaytime<200 ? 200 : delaytime
delayDo(arr, v => cb(v), delaytime);
}
// 插入元素,间隔 200 毫秒
main([...Array(20).keys()].map(v=>v+1), process.insert, 800);
参考资料
笑虾:数据结构 - 学习笔记 - 红黑树前传——234树
www.cs.usfca.edu:Red/Black Tree 数据结构可视化
Programiz:Red-Black Tree
Algorithmtutor:Red Black Trees (with implementation in C++, Java, and Python)