红黑树 (Red-Black Tree)
接前文分析,本文先实现RBT中结点的旋转方法,然后对向红黑树中插入新结点的时候的各种情况逐一分析,最终完成红黑树上插入新结点的insert方法。
1. BST结点左旋
根据之前的分析,如下图:
--→ FR---
↑ ↓
FRL ←----F
简记:所谓左旋指的就是-> 父结点作为其右子结点的左子结点
对应的实现代码如下:
// 调整策略一:左旋转--指的是当前结点的位置被其右子结点取代,而它本身成为其右子结点的左子结点
// 此函数执行完毕之后node的位置会发生变化
leftRotate(node) {
// 第一步:找相关结点
// 找到右子结点FR
const rightChild = node.right;
// 第二步:处理孙子结点的位置
// 如果FRL存在就作为F的子结点
node.right = rightChild.left;
if (rightChild.left !== null) {
rightChild.left.parent = node;
}
// 第三步:使用右子结点替换位置
// 然后使用FR替代F的位置
rightChild.parent = node.parent;
// 处理F的原来的父结点的子结点问题
// 如果F原来的父结点为null,则证明node就是root
if (node.parent === null) {
this.root = rightChild;
// 否则用FR替换F
} else if (node === node.parent.left) {
node.parent.left = rightChild;
} else {
node.parent.right = rightChild;
}
// 第四步:作为右子结点的左子结点
// F作为其原来右子结点的左子结点
rightChild.left = node;
node.parent = rightChild;
}
2. BST结点右旋
根据之前的分析,如下图:
--→ FL---
↑ ↓
FLR ←----F
简记:所谓右旋指的就是-> 父结点作为其左子结点的右子结点
对应的实现代码如下:
// 调整策略二:右旋转--指的是当前结点的位置被其左子结点取代,而它本身成为其左子结点的右子结点
// 此函数执行完毕之后node的位置会发生变化
rightRotate(node) {
// 第一步:找到相关结点FL
const leftChild = node.left;
// 第二步:处理孙子结点的位置
node.left = leftChild.right;
// 如果左子结点的右子结点存在,则将其挂在本结点下面
if (leftChild.right !== null) {
leftChild.right.parent = node;
}
// 第三步:使用FL取代F的位置
leftChild.parent = node.parent;
// 处理本结点是root的情况
if (node.parent === null) {
this.root = leftChild;
// 如果原来的位置不是root则用FL替换原来的位置
} else if (node === node.parent.left) {
node.parent.left = leftChild;
} else {
node.parent.right = leftChild;
}
// 第四步:调整F的位置
leftChild.right = node;
node.parent = leftChild;
}
旋转的一些结论
-
- 结点左旋是为了让自己的右子结点上位;
-
- 右旋是为了让自己的左子结点上位。
3. BST插入结点策略
- 总体策略:先将新的结点作为一个红色结点插入进去,然后根据插入之后的结果对树进行检查并调整;简称先插后查;
- 格式检查方法,检查某一个结点的局部是否符合红黑树的特性,如果不符合则根据一定的规则调整;
- 所谓
结点N的局部,指的就是:父结点P,叔叔结点U,祖父结点G; - 理论上,插入新的结点之后应该对每个现有结点进行检查,但是大多数情况下新插入的结点对树的扰动是有限的,所以无需对每个结点都进行检查;
- 为方便,相比较于BST中的node对象,RBT中的node对象会多一个parent属性指向自己的父结点;
- 尽管插入之后的情况比较复杂,但是是可以穷举的,只需要通过分类讨论解决每一个可能就行了;
下面是插入结点(但是不包括结点位置检查)的代码实现:
// 插入结点
insert(value) {
const newNode = new Node(value);
// 传统的BST中结点不会保持对父结点的引用,所以再遍历的时候始终使用一个变量保存当前结点的父结点
let current = this.root;
let parent = null;
// 传统的BST新插入的结点一定是叶子结点,所以只需使用while循环往最后一层找就可以了
while (current !== null) {
parent = current;
if (newNode.value < current.value) {
current = current.left;
} else {
current = current.right;
}
}
// 找到最后一层也就知道了自己的父结点是谁
newNode.parent = parent;
// 然后处理新插入的结点到底是左子结点还是右子结点的问题
// 处理空树情况
if (parent === null) {
this.root = newNode;
} else if (newNode.value < parent.value) {
// 通过和根结点比较大小确定插入的结点时左结点还是右结点
parent.left = newNode;
} else {
parent.right = newNode;
}
// 先尝试性的将红色结点插入进去,然后再根据插入之后的结果对树进行调整
this.fixInsertViolation(newNode);
}
4. 新插入结点影响的有限性
如何界定新插入的结点对树的影响是有限的呢?有如下结论:
只有旋转和变色的操作,改变了G的颜色,此时新增结点产生的影响才会向上层结点传播
5. N P G 之间的关系 -- 撇捺顺柺
“撇捺顺柺”是我自己想出来的,用来记忆“N P G” 之间的关系的一种简称:
-
- 撇 N P都是左子结点
-
- 捺 N P都是右子结点
-
- 顺 N P关系位撇或者捺
-
- 柺 N P的子结点方向相反
P.S. 需要对P旋转将柺变成顺
- 柺 N P的子结点方向相反
6. 谁会旋转
在检查结点方法中,只涉及两种旋转,它们及它们的特点是:
-
- 父结点的旋转:和其位置相同(即如果P是左子结点则向左旋转),父结点旋转前不变色,旋转完之后此位置变成黑色,相邻的两个结点都变成红色;
-
- 祖父结点的旋转:为了让父结点上位,所以父在右祖左旋,父在左祖右旋。祖父结点旋转后,各个结点均不变色。
7. 被检验的结点所有可能出现的位置关系
N: 表示被检测的结点P: N的父结点U: P的兄弟结点G: P的父结点
-
- N本身为null;
-
- N的父亲为null,这说明被检测对象是root结点;
-
- P为黑色;
- P为红色:
- P为黑色;
-
- P为红色,U也为红色;
x. P为红色,U为黑色;
- P为红色,U也为红色;
-
- P为红色,U不存在;
- 4.1 P为左子结点
- (1)N为左子结点 撇
- (2)N为右子结点 右柺
- 4.2 P为右子结点
- (1)N为右子结点 捺
- (2)N为左子结点 左柺
- P为红色,U不存在;
8. 各种情况解决方法
- case 0:
- 显然这种情况下什么都不用做; - case 1:
- 这种情况下需要保证被检测对象是黑色,所以需要红变黑; - case 2:
- P为黑色时插入一个红色节点是符合规定的,什么都不需要做; - case 3:
- 将P和U都从红变成黑,将G从黑变成红(注意此时G一定是黑的),然后递归检查G结点; - case x:
- P为红色时G一定是黑色,此时如果U也为黑色,则从G出发到达P子树叶子节点和U子树叶子结点的黑色结点数目必不相同,所以这种情况不存咋! - case 4.1:
- 如果右拐,先左旋P(见:6. 谁会旋转)纠正右拐为撇,然后变色,最后右旋G; - case 4.2:
- 如果左拐,先右旋P(见:6. 谁会旋转)纠正左拐为捺,然后变色,最后左旋G;
结点位置检测代码如下:
// 对红黑树中的某个结点的位置进行检查
fixInsertViolation(node) {
// case 0:
if(!node) return;
// 爸爸进场
const parent = node.parent;
// case 1:
if(!parent) {
node.color = 'black';
return;
}
// case 2:
if(parent.color === "black") return;
// 爷爷进场
const grandP = parent.parent;
// 叔叔进场
const _tmp = grandP.left;
const uncle = _tmp === parent ? grandP.right : _tmp;
// case 3:
if(uncle?.color === "red") {
parent.color = "black";
uncle.color = "black";
this.fixInsertViolation(grandP);
}
// 判断P的位置
const isPLeft = P === grandP.left;
// 判断N的位置
const isNLeft = N === P.left;
if(isPLeft) {
// case 4.1:
// 先处理右拐
if(!isNLeft){
this.leftRotate(parent);
// 然后变色 -- 注意此时N和F的位置调换了
node.color = "black";
} else {
// G右旋之前需要先变色
parent.color = "black";
}
grandP.color = "red";
this.rightRotate(grandP);
} else {
// case 4.2:
if(isNLeft){
// 先处理左拐
this.rightRotate(parent);
// 然后变色 -- 注意此时N和F的位置调换了
node.color = "black";
} else {
// G左旋之前需要先变色
parent.color = "black";
}
grandP.color = "red";
this.leftRotate(grandP);
}
}
9. 扰动传播分析
- 对于case 0, 1, 2, 均不涉及G变色问题,所以检查时候改变树的结构不会将扰动传播到上层结点中;
- 对于case 4, 画图分析之后发现旋转G不会造成G所在的位置的结点的颜色发生改变,所以扰动同样也不会传播到上层结点;
- 对于case 3, 由于G从黑变成了红色,所以可以将G子树整体看成是一个红色结点,此时对原来的N的检查已经结束,开始检查G结点,通过这样的方式将扰动传递到了上层结点中。
10. 实现代码
// 定义红黑树结点
class Node {
constructor(value, color) {
this.value = value;
this.color = color || "red";
this.left = null;
this.right = null;
this.parent = null;
}
}
// 定义红黑树
class RedBlackTree {
constructor() {
this.root = null;
}
// 调整策略一:左旋转--指的是当前结点的位置被其右子结点取代,而它本身成为其右子结点的左子结点
// 此函数执行完毕之后node的位置会发生变化
leftRotate(node) {
// 第一步:找相关结点
// 找到右子结点FR
const rightChild = node.right;
// 第二步:处理孙子结点的位置
// 如果FRL存在就作为F的子结点
node.right = rightChild.left;
if (rightChild.left !== null) {
rightChild.left.parent = node;
}
// 第三步:使用右子结点替换位置
// 然后使用FR替代F的位置
rightChild.parent = node.parent;
// 处理F的原来的父结点的子结点问题
// 如果F原来的父结点为null,则证明node就是root
if (node.parent === null) {
this.root = rightChild;
// 否则用FR替换F
} else if (node === node.parent.left) {
node.parent.left = rightChild;
} else {
node.parent.right = rightChild;
}
// 第四步:作为右子结点的左子结点
// F作为其原来右子结点的左子结点
rightChild.left = node;
node.parent = rightChild;
}
// 调整策略二:右旋转--指的是当前结点的位置被其左子结点取代,而它本身成为其左子结点的右子结点
// 此函数执行完毕之后node的位置会发生变化
rightRotate(node) {
// 第一步:找到相关节点FL
const leftChild = node.left;
// 第二步:处理孙子结点的位置
node.left = leftChild.right;
// 如果左子结点的右子结点存在,则将其挂在本结点下面
if (leftChild.right !== null) {
leftChild.right.parent = node;
}
// 第三步:使用FL取代F的位置
leftChild.parent = node.parent;
// 处理本结点是root的情况
if (node.parent === null) {
this.root = leftChild;
// 如果原来的位置不是root则用FL替换原来的位置
} else if (node === node.parent.left) {
node.parent.left = leftChild;
} else {
node.parent.right = leftChild;
}
// 第四步:调整F的位置
leftChild.right = node;
node.parent = leftChild;
}
// 可以看出来,不管是左旋还是右旋都是先改变层级比较深的结点的位置,然后才是一层一层往上修改的
// 插入结点
insert(value) {
const newNode = new Node(value);
// 传统的BST中结点不会保持对父结点的引用,所以再遍历的时候始终使用一个变量保存当前结点的父结点
let current = this.root;
let parent = null;
// 传统的BST新插入的结点一定是叶子结点,所以只需使用while循环往最后一层找就可以了
while (current !== null) {
parent = current;
if (newNode.value < current.value) {
current = current.left;
} else {
current = current.right;
}
}
// 找到最后一层也就知道了自己的父结点是谁
newNode.parent = parent;
// 然后处理新插入的结点到底是左子结点还是右子结点的问题
// 处理空树情况
if (parent === null) {
this.root = newNode;
} else if (newNode.value < parent.value) {
// 通过和根结点比较大小确定插入的结点时左结点还是右结点
parent.left = newNode;
} else {
parent.right = newNode;
}
// 先尝试性的将红色结点插入进去,然后再根据插入之后的结果对树进行调整
this.fixInsertViolation(newNode);
}
// 对红黑树中的某个结点的位置进行检查
fixInsertViolation(node) {
// case 0:
if(!node) return;
// 爸爸进场
const parent = node.parent;
// case 1:
if(!parent) {
node.color = 'black';
return;
}
// case 2:
if(parent.color === "black") return;
// 爷爷进场
const grandP = parent.parent;
// 叔叔进场
const _tmp = grandP.left;
const uncle = _tmp === parent ? grandP.right : _tmp;
// case 3:
if(uncle?.color === "red") {
parent.color = "black";
uncle.color = "black";
this.fixInsertViolation(grandP);
}
// 判断P的位置
const isPLeft = P === grandP.left;
// 判断N的位置
const isNLeft = N === P.left;
if(isPLeft) {
// case 4.1:
// 先处理右拐
if(!isNLeft){
this.leftRotate(parent);
// 然后变色 -- 注意此时N和F的位置调换了
node.color = "black";
} else {
// G右旋之前需要先变色
parent.color = "black";
}
grandP.color = "red";
this.rightRotate(grandP);
} else {
// case 4.2:
if(isNLeft){
// 先处理左拐
this.rightRotate(parent);
// 然后变色 -- 注意此时N和F的位置调换了
node.color = "black";
} else {
// G左旋之前需要先变色
parent.color = "black";
}
grandP.color = "red";
this.leftRotate(grandP);
}
}
// 中序遍历红黑树
inOrderTraversal(node, handle) {
if (node !== null) {
this.inOrderTraversal(node.left, handle);
handle(node);
this.inOrderTraversal(node.right, handle);
}
}
}
// 测试红黑树
const redBlackTree = new RedBlackTree();
redBlackTree.insert(10);
redBlackTree.insert(20);
redBlackTree.insert(30);
redBlackTree.insert(15);
redBlackTree.insert(25);
redBlackTree.inOrderTraversal(redBlackTree.root, console.log);