前言:找博客学习红黑树,发现有些比较晦涩难懂,有些讲的不清不楚。幸运地在B站找到一部视频,按照算法导论讲的例子,听懂了。所以好的教材挺重要的。
然后决定自己写一篇博客讲解一下我的理解和实现,希望能帮助到有需要的小伙伴。
视频链接:1小时搞定红黑树_哔哩哔哩_bilibili,感兴趣的可以去看看
抱歉,写完之后发现图里的树画得不严谨,不完整,看上去不是平衡的,但是我也懒得改了。请大家主要考虑情况和解决方案即可。可自行画一个完整的红黑树,再按照解决方案进行操作、思考。
二叉搜索树
首先了解一下二叉搜索树,它的规则就是父节点的左子树上的点都要比它“小”,右子树上的右子树上的点都要比它“大”。左右顺序不重要,重要的是要有这个顺序,让它是一颗有序的二叉树。当它有序的时候且是一个完全平衡的时候(即除了最后一层,其它层都是满的,没有空节点),它的搜索效率是最高的。类似于二分搜索,时间复杂度是O(logN)。
查找:类似于二分法,比如要找5,则7-->3-->5。找到返回。
插入:找到合适的位置,再插入。比如插入10,则 7-->9-->13,比13小,插入左子节点。
删除:
如果目标节点没有子节点如1,则直接删除即可。
如果目标节点有1个子节点如13,则删除13,将节点10顶替上去。
如果目标节点有2个子节点,则取其中序遍历的前驱节点或后继节点作为实际删除节点,先交换值,再进行删除操作。如节点9,其中序遍历的后继节点为10,则两节点交换值,再进行删除操作。
修改:即先做删除,再做插入操作。
至此,二叉搜索树的四种操作讲解完毕,它们的处理都是为了保证二叉搜索树的有序性。
左旋和右旋
再了解一下二叉搜索树的左旋和右旋。
左旋:
以目标节点为支点进行向左的旋转。
条件是目标节点不为空,且右子节点不为空。如下图是以节点a为支点进行左旋。
右旋:
以目标节点为支点进行向右的旋转。
条件是目标节点不为空,且右子节点不为空。如下图是以节点b为支点进行旋转。
左旋和右旋是一个对称的操作, 过程比较简单,我也就不做赘述了,相信大家看一看就能理解,这2步的操作,也保证了二叉搜索树的有序性。
红黑树
基础讲完现在进入红黑树的内容,红黑树也是一颗二叉搜索树。
二叉搜索树越接近完全平衡,则其查找效率越高。当你进行了删除或插入操作的时候,会破坏平衡,此时则需要进行调整平衡的操作。当然可以通过一系列操作重新调整为完全平衡,但是操作会很繁琐,在频繁插入和删除的时候代价过大。
红黑树则利用颜色来限制自身的高度,来实现了一种近乎平衡。
颜色条件:
1、节点有颜色,红色或者黑色。
2、根节点必须为黑色,空节点视为黑色。
3、红色节点不能相连。
4、以任意节点为起点,到它的叶子节点上的黑色数目要相等。
从根节点出发,到叶子节点的黑色节点数目为N,则其高度不会超过2N。
下面来讲红黑树是如果调整颜色的平衡,关注插入和删除即可。查找不改变节点,修改则是先删除后插入。
颜色平衡的主要思想是要么向上推进,因为到根节点一定能解决,要么就是往可解决的状态进行调整。理解这句话,则可以理解后续的所有操作,后续的操作基本是递归处理。
插入操作
插入操作首先按照二叉搜索树的规则,进行插入。然后再进行调整,使颜色平衡。
插入的节点为红色,然后根据以下几种情况分别处理,下面的()里会说明一些确定的信息。
1、根节点为空,则插入的节点作为根节点,设为黑色即可。
2、父节点为黑色,直接平衡,无需处理。
3、父节点为红色(此时祖父节点必为黑色)。
3.1 叔节点为红色。
3.2 叔节点为黑色,插入节点与父节点不同侧,不同侧指父节点是祖父节点的左(右)节点,而插入节点是父节点的右(左)节点。后文不再做赘述。
3.3 叔节点为黑色,插入节点与父节点同侧。
情况1、2很简单,跳过。然后来讲情况3中的具体处理。
3.1 父节点为红色,叔节点为红色。此时要做的操作是,将父节点和叔节点设为黑色,祖父节点设为红色,当祖父节点是根节点时,仍为黑色。此时,祖父节点的左右子树颜色已经平衡,然后将祖父节点视为新的目标节点,根据情况1、2、3进行处理。
这一步可视为将祖父节点的黑色分给了父、叔两个子节点。目的就是把问题节点向上推进。
3.2 父节点为红色,叔节点为黑色,目标节点与父节点不同侧。此时要做的操作是,以父节点为支点进行旋转操作。当目标节点在右侧,则左旋,当目标节点在左侧,则右旋。下图以前者为例。
旋转完之后,两者同侧。再以父节点作为新的目标节点,此时则进入到了情况3.3,这也是我们的目的。
3.3 父节点为红色,叔节点为黑色,目标节点与父节点同侧(该情况有可一步解决的方案)。此时的操作是,祖父节点与父节点颜色进行叫唤,然后以祖父节点为支点,进行旋转。父节点在左侧,则右旋,在右侧则左旋。下图以前者为例。
上面2幅图的没有名字的黑色节点是为了提醒大家,旋转的时候,如果该子节点不为空,记得要做对应的处理。
至此,插入情况处理完毕。
删除操作
首先进行二叉搜索树的删除操作,再调整颜色使其符合规则。
当目标节点左右子节点均不会空时,我们取中序遍历的后继节点为实际删除节点。将目标节点和实际删除节点交换值,颜色不变。此时实际删除节点即可视为目标节点,再进行删除操作。后文的目标节点
显然目标节点至多有1个子节点,至少有1个节点是空节点,视为黑色。然后我们做一个逻辑上的约定,删除目标节点之后,会遗留颜色在当前位置,它的子节点会顶替上去,然后当前子节点的颜色视为遗留颜色 和 自身颜色的相加得到的和。如果两个子节点为空,则视为空节点顶替上去。
如上图所示,灰色表示未知节点,x表示黑或者红。比如当父节点为黑色,子节点为红色时,删除父节点,子节点顶替,则节点的颜色为红+黑。 此时这个有2种颜色的节点在该支路上暂时仍是平衡的,但我们要做的是如何把多的这个颜色处理掉,因为一个节点只能有一个颜色。
根据这种设定,我们有两种情况需要处理。
1、红+黑
2、黑+黑
2.1 目标节点为根节点
2.2 兄弟节点是黑色,且其2个子节点也是黑色
2.3 兄弟节点是黑色,且其同侧子节点是黑色
2.4 兄弟节点是黑色,且其同侧子节点是红色
下面,我们来根据情况一个一个处理。
1、红+黑
此时直接丢弃红色即可,显然不会破坏左右子树的平衡。
2.1 目标节点为根节点
此时丢弃1个黑色即可,相当于所有路径上都减少了1个黑色节点,仍保持平衡。
理论上上图情况不会出现,黑+黑且为根节点,我的理解是当只有1个根节点,删除根节点的情况下才会出现。此图只是便于理解,无需较真。
2.2 兄弟节点是黑色,且其2个子节点也是黑色
2个节点可能都是黑节点,也可以都是空节点(空节点视为黑色)。此时要做的操作是将兄弟节点置为红色,目标节点置为黑色,父节点置为x+黑。此时将父节点视为问题节点,再根据情况1、2进行处理。这一步可视为将目标节点和兄弟节点的黑色往上推了1层,给了父节点。目的就是将问题节点上移。
2.3 兄弟节点是黑色,且同侧子节点是黑色
情况的顺序是按优先级来考虑的,所以在不满足2.2的情况下,此时必有1个子节点是红色。按照兄弟节点在右侧进行处理,若左侧则进行对应操作即可。
此时要做的操作是,左子节点和兄弟节点颜色交换,以兄弟节点为支点进行右旋操作。此时得到了一个新的兄弟节点为黑色,且它的同侧子节点为红色。符合情况2.4,这也是我们的目的。
2.4 兄弟节点是红色,且其同侧子节点是红色
这一步是有解决方案的。这也是为什么要从2.3调整至2.4。
此时要做的操作是将兄弟节点与父节点交换颜色,然后以父节点为支点进行左旋。将目标节点上多的1个黑色,赋值给兄弟节点的同侧红色子节点。
至此颜色平衡。
图画得不严谨,不完整,看上去不是平衡的。比如父节点右侧,可以考虑调整前左侧是平衡的,那兄弟节点的左子树必然是平衡的,进行左旋之后,它就接在了父节点的右边,此时当然也是平衡的。
主要理解情况和思想即可。
红黑树的实现(TypeScript)
/** 节点类 */
export class TreeNode {
/** 键 */
public key: number;
/** 值 */
public value: number;
/** 是否是红色 */
public is_red: boolean;
/** 左子节点 */
public left: TreeNode;
/** 右子节点 */
public right: TreeNode;
/** 父节点 */
public parent: TreeNode;
/**
* @param key: 键
* @param value 节点值
* @param is_red 是否是红色
* @param left 左子节点
* @param right 右子节点
* @param parent 父节点(为空则为根)
*/
constructor(key: number, value: number, is_red: boolean = true, left?: TreeNode, right?: TreeNode, parent?: TreeNode) {
this.key = key;
this.value = value;
this.is_red = is_red;
this.left = left;
this.right = right;
this.parent = parent;
}
}
/** 红黑树 */
export class RBTree {
/** 根节点 */
public root: TreeNode;
/**
* @param root 根节点
*/
constructor(root?: TreeNode) {
this.root = root;
}
/**
* 查找一个节点
* @param key 键
* @returns 返回目标节点,若为null,则未找到
*/
public find(key: number): TreeNode {
let node: TreeNode = this.root;
let is_find: boolean = false;
while (node != null) {
if (node.key === key) {
is_find = true;
break;
} else if (node.key > key) {
node = node.left;
} else if (node.key < key) {
node = node.right;
}
}
return is_find ? node : null;
}
/**
* 插入节点
* @param node 新增节点
* @returns 是否插入成功
*/
public insert(node: TreeNode): boolean {
node.parent = null;
node.left = null;
node.right = null;
node.is_red = true;
// 如果根为空,则置为根节点
if (this.root == null) {
node.is_red = false;
this.root = node;
return true;
}
let parent_node: TreeNode = this.root;
while (parent_node != null) {
// 如果key重复,则插入失败
if (parent_node.key === node.key) {
return false;
}
// 向左或者右寻找合适的父节点插入
if (parent_node.key > node.key) {
if (parent_node.left == null) {
parent_node.left = node;
node.parent = parent_node;
break;
} else {
parent_node = parent_node.left;
}
} else {
if (parent_node.right == null) {
parent_node.right = node;
node.parent = parent_node;
break;
} else {
parent_node = parent_node.right;
}
}
}
this.adjustColorAferInsert(node);
return true;
}
/**
* 插入之后调整颜色
* @param node 插入的节点
*/
private adjustColorAferInsert(node: TreeNode) {
// 1.插入是根节点或其父节点是黑色
if (node.parent == null || node.parent.is_red == false) {
return;
}
let parent_node: TreeNode = node.parent;
let grandparent_node: TreeNode = parent_node.parent;
let is_left: boolean = grandparent_node.left == parent_node;
let uncle_node: TreeNode = is_left ? grandparent_node.right : grandparent_node.left;
// 2.父节点是红色且叔节点是红色,则将祖父节点的黑色分发下来,将父节点置为新的问题节点,继续递归,根节点除外
if (uncle_node != null && uncle_node.is_red == true) {
parent_node.is_red = false;
uncle_node.is_red = false;
if (grandparent_node == this.root) {
return;
} else {
grandparent_node.is_red = true;
return this.adjustColorAferInsert(grandparent_node);
}
}
// 3.父节点是红色且叔节点是黑色(空节点视为黑色),子节点和父节点不同侧,以父节点为中心进行旋转,变成同侧,继续递归
if ((uncle_node == null || uncle_node.is_red == false) && (node == parent_node.left) !== is_left) {
is_left ? this.leftRotate(parent_node) : this.rightRotate(parent_node);
return this.adjustColorAferInsert(parent_node);
}
// 4.父节点是红色且叔节点是黑色(空节点视为黑色),子节点和父节点同侧,父节点和祖父节点交换颜色,进行对应的旋转即可
if ((uncle_node == null || uncle_node.is_red == false) && (node == parent_node.left) === is_left) {
parent_node.is_red = !parent_node.is_red;
grandparent_node.is_red = !grandparent_node.is_red;
is_left ? this.rightRotate(grandparent_node) : this.leftRotate(grandparent_node);
}
return;
}
/**
* 删除某个key值节点
* @param key 键值
* @returns 是否删除成功
*/
public delete(key: number): boolean {
let node: TreeNode = this.find(key);
if (node == null) {
return false;
}
// 先找到要删除的节点
// 如果目标节点左右子节点不为空,则取中序遍历的后继节点为目标节点
// 交换key与值,不交换颜色
let temp_node: TreeNode = node;
if (node.right != null) {
node = node.right;
}
if (node.left != null && node.right != null) {
while (node.left != null) {
node = node.left;
}
}
let temp_value = temp_node.value;
temp_node.value = node.value;
node.value = temp_value;
let temp_key = temp_node.key;
temp_node.key = node.key;
node.key = temp_key;
// 假定删除,进行颜色的调整
// 此时删除节点最多只有1个子节点
// 该删除节点与顶替节点颜色做相加处理(空节点视为黑色)
let parent_node: TreeNode = node.parent;
let is_left: boolean = parent_node != null && parent_node.left == node;
let child_node: TreeNode = node.left != null ? node.left : node.right;
let is_red: boolean = child_node != null ? child_node.is_red : false;
this.adjustColorAfterDelete(node, is_red);
// 颜色调整完毕,实际进行删除的操作
// 删除节点,保留颜色
if (parent_node != null) {
is_left == true ? parent_node.left = child_node : parent_node.right = child_node;
} else {
this.root = child_node;
}
if (child_node != null) {
child_node.is_red = false; // 顶替节点最终必然是黑色
child_node.parent = parent_node;
}
node.parent = null;
node.left = null;
node.right = null;
return true;
}
/**
* 删除节点之后调整颜色
* @param node 替换节点
* @param is_red 遗留颜色
*/
private adjustColorAfterDelete(node: TreeNode, is_red: boolean) {
// 1.红+黑,直接返回
// 2.黑加黑,但为根节点,直接返回
if (node == this.root || node.is_red !== is_red) {
node.is_red = false; // 这一步是为了处理在情况4之后,直接红+黑的情况
return;
}
let parent_node: TreeNode = node.parent;
let is_left: boolean = node == parent_node.left;
let brother_node: TreeNode = is_left ? parent_node.right : parent_node.left;
// 黑加黑的情况下,兄弟节点必不为空
// 3.兄弟节点是红色, 父节点必为黑色,两者交换颜色,以父节点为支点进行旋转,新的兄弟节点必为黑色,递归
if (brother_node.is_red === true) {
brother_node.is_red = false;
parent_node.is_red = true;
is_left ? this.leftRotate(parent_node) : this.rightRotate(parent_node);
return this.adjustColorAfterDelete(node, is_red);
}
// 4.兄弟节点是黑色,其2个子节点也是黑色,兄弟节点置为红色,将多的1个黑色上移至父节点,递归
if ((brother_node.left == null || brother_node.left.is_red === false) && (brother_node.right == null || brother_node.right.is_red == false)) {
brother_node.is_red = true;
return this.adjustColorAfterDelete(parent_node, false);
}
// 5.兄弟节点是黑色,其同侧的子节点是黑色,不同侧子节点是红色,则交换颜色,进行相应的旋转,递归,目的是让同侧子节点是红色
if (is_left === true && (brother_node.right == null || brother_node.right.is_red === false)) {
brother_node.left.is_red = false;
brother_node.is_red = true;
this.rightRotate(brother_node);
return this.adjustColorAfterDelete(node, is_red);
}
if (is_left === false && (brother_node.left == null || brother_node.left.is_red === false)) {
brother_node.right.is_red = false;
brother_node.is_red = true;
this.leftRotate(brother_node);
return this.adjustColorAfterDelete(node, is_red);
}
// 6.兄弟节点是黑色,其同侧子节点是红色,同侧子节点置为黑色,兄弟节点与父节点交换颜色,进行旋转,Over
// 此时必然满足条件无需判断
is_left === true ? brother_node.right.is_red = false : brother_node.left.is_red = false;
brother_node.is_red = parent_node.is_red;
parent_node.is_red = false;
is_left === true ? this.leftRotate(parent_node) : this.rightRotate(parent_node);
}
/**
* 修改节点的值
* @param key 节点的键
* @param value 新的值
* @returns 是否修改成功
*/
public change(key: number, value: number): boolean {
let node: TreeNode = this.find(key);
if (node != null) {
node.value = value;
return true;
}
return false;
}
/**
* 左旋
* @param node 中心点
* @returns 是否左旋成功
*/
private leftRotate(node: TreeNode): boolean {
// 当前节点为空或右子节点为空,无法左旋
if (node == null || node.right == null) {
return false;
}
let parent_node: TreeNode = node.parent;
let right_child: TreeNode = node.right;
// 将右子节点与父节点相连
if (parent_node != null) {
parent_node.left == node ? parent_node.left = right_child : parent_node.right = right_child;
}
right_child.parent = parent_node;
// 调整当前节点与右子节点的关系
node.parent = right_child;
node.right = right_child.left;
right_child.left = node;
// 判断根节点是否改变
if (right_child.parent == null) {
this.root = right_child;
}
return true;
}
/**
* 右旋
* @param node 中心点
* @returns 是否右旋成功
*/
private rightRotate(node: TreeNode): boolean {
// 当前节点为空或左子节点为空,无法右旋
if (node == null || node.left == null) {
return false;
}
let parent_node: TreeNode = node.parent;
let left_child: TreeNode = node.left;
// 将右子节点与父节点相连
if (parent_node != null) {
parent_node.left == node ? parent_node.left = left_child : parent_node.right = left_child;
}
left_child.parent = parent_node;
// 调整当前节点与右子节点的关系
node.parent = left_child;
node.left = left_child.right;
left_child.right = node;
// 判断根节点是否改变
if (left_child.parent == null) {
this.root = left_child;
}
return true;
}
}