小编会一直更新数据结构相关方面的知识,使用的语言是Java,但是其中的逻辑和思路并不影响,如果感兴趣可以关注合集。
希望大家看完之后可以自己去手敲实现一遍,同时在最后我也列出一些基本和经典的题目,可以尝试做一下。大家也可以自己去力扣或者洛谷牛客这些网站自己去练习,数据结构光看不敲是学不好的,加油,祝你早日学会数据结构这门课程。
知不足而奋进,望远山而前行。
概述
在开始之前我想说一下,红黑树其实是算比较难的数据结构了,但是要讲清楚红黑树的特性并没有那么难,所以红黑树难就难在它的实现上,所以手撕红黑树这件事,不能只有理论,更多应该去实践,正所谓纸上得来终觉浅 绝知此事要躬行。
本次我们学习的红黑树也是一种自平衡的二叉搜索树,和上一次我们学习的AVL树相比,红黑树的插入和删除时旋转次数更少。红黑树这一块还是有一定难度的,需要深刻理解上一节所介绍的旋转机制。
红黑树有以下这些特性:
1. 所有节点都有两种颜色:红与黑
2. 所有null都视为黑色
3. 红色节点不能相邻
4. 根节点是黑色
5. 从根到任意一个叶子节点,路径中的黑色节点数一样(黑色完美平衡)
下图所示就是一个红黑树,对于上面五种特性,其实都是满足的。
上面这五条特性多看几遍,因为下面操作时都要遵循特性才能实现出平衡的红黑树。
实现
结构
红黑树的结构在节点上其实也就是多了一个颜色的属性。但是红黑树的删除操作完美需要用到父节点,所以完美额外还需要一个变量去记录当前节点的父节点,另外颜色属性初始化为红色,也就是每个节点一开始默认就是红色。
public class RedBlackTree {
enum Color {
Red, Black;
}
private Node root;
private static class Node{
int key;
int value;
Node left;
Node right;
Node parent; // 父节点
Color color = Color.Red; // 颜色
}
}
工具方法
熟悉完红黑树的结构后,我们也同样来实现几个好用的工具方法,目的是后续实现crud时可以调用工具方法来解决一些代码的复用以及提升代码的健壮性。
首先我们来实现一个判断当前节点是否是左孩子的方法,这个因为我们记录了当前节点的父节点所以很好判断。
// 是不是左孩子
boolean isLeftChild() {
return parent != null && parent.left == this;
}
接着我们来实现一个找叔叔节点的方法,同样我们知道父节点所以这个方法实现起来也是非常简单的。
// 找到叔叔节点
Node uncle() {
if (parent == null || parent.parent == null) {
return null;
}
if (parent.isLeftChild()) {
return parent.parent.right;
} else {
return parent.parent.left;
}
}
第三个工具方法,我们来实现找到当前节点的兄弟节点,这个对于大家来说相信也是轻轻又松松,舒舒又服服。
// 找到兄弟节点
Node sibling() {
if (parent == null) {
return null;
}
if (this.isLeftChild()) {
return parent.right;
} else {
return parent.left;
}
}
第四个工具方法我们来判断当前节点是否是红色还是黑色,这个方法主要是增强代码健壮性,不要让外界直接可以访问颜色属性。
// 判断节点是否是红色
boolean isRed(Node node) {
return node != null && node.color == Color.Red;
}
// 判断节点是否是黑色
boolean isBlack(Node node) {
// return !isRed(node);
return node == null || node.color == Color.Black;
}
既然红黑树也是自平衡的树形结构,那么红黑树底层也是要通过旋转机制来实现自平衡的,所以接下来我们来实现一下红黑树的左旋和右旋。其实大部分逻辑和前面的还是一样的,不同的就是我们这次节点多了一个parent属性,所以在旋转时候要处理好parent指向,第二个不同的就是之前我们是通过返回值的形式返回新的根节点,这一次我们没有返回值,我们是要在旋转内就把新根的父子关系建立好。
// 右旋 1. parent 的处理 2. 没有返回值
private void rightRotate(Node node) {
Node parent = node.parent;
Node leftNode = node.left;
Node leftNodeRight = leftNode.right;
if (leftNodeRight != null) {
leftNodeRight.parent = node;
}
leftNode.right = node;
leftNode.parent = parent;
node.left = leftNodeRight;
node.parent = leftNode;
if (parent == null) {
root = leftNode;
}
if (parent.left == node) {
parent.left = leftNode;
} else {
parent.right = leftNode;
}
}
// 左旋
private void leftRotate(Node node) {
Node parent = node.parent;
Node rightNode = node.right;
Node rightNodeLeft = rightNode.left;
if (rightNodeLeft != null) {
rightNodeLeft.parent = node;
}
rightNode.left = node;
rightNode.parent = parent;
node.right = rightNodeLeft;
node.parent = rightNode;
if (parent == null) {
root = rightNode;
}
if (parent.left == node) {
parent.left = rightNode;
} else {
parent.right = rightNode;
}
}
对照上面这颗红黑树来看左旋和右旋代码,相信这对有了旋转基础的你来说没有问题。
如果有对左旋后右旋代码一点也不熟悉的可以看我上一篇AVL树,那里对于旋转机制做了详细的图文解释。
新增
其实网上有许多不同的红黑树讲解方式,比如什么左倾红黑树,2-3树等等,这里我就是给出经典的红黑树的架子,了解了基础才可以更好去学习别的实现。
红黑树的新增操作其实和前面讲解的树形结构的新增基本上都是一样的,正常删除,不同的就是当遇到红红不平衡时进行调整。在代码层面不同的就是我们在插入节点时我们需要将新节点的parent属性也设置一下,另外当新增节点与它的父节点都是红色的时候,这就是红红不平衡,这时就需要进行调整。调整策略其实前人也已经给出策略了,我们只需要根据策略实现代码就好了。
以下就是红黑树的插入代码,实现起来还是有些复杂的,处理好逻辑,情况一个一个处理相信你也可以实现出来的。
// 新增或更新
// 正常增,遇到红红不平衡进行调整。
public void put(int key, int value) {
Node p = root;
Node parent = null;
while (p != null) {
parent = p;
if (key < p.key) {
p = p.left;
} else if (key > p.key) {
p = p.right;
} else {
p.value = value; // 更新
return;
}
}
Node inserted = new Node(key, value);
if (parent == null) {
root = inserted;
} else if (key < parent.key) {
parent.left = inserted;
inserted = parent;
} else {
parent.right = inserted;
inserted = parent;
}
fixRedRed(inserted);
}
void fixRedRed(Node x) {
// case 1 插入节点时根节点,变黑即可
if (x == root) {
x.color = Color.Black;
return;
}
// case 2 插入节点父亲是黑色,无需调整
if (isBlack(x.parent)) {
return;
}
Node parent = x.parent;
Node uncle = x.uncle();
Node grandparent = parent.parent;
/* case 3 叔叔为红色
需要将父亲,叔叔变黑,祖父变红,然后对祖父做递归处理
*/
if (isRed(uncle)) {
parent.color = Color.Black;
uncle.color = Color.Black;
grandparent.color = Color.Red;
fixRedRed(grandparent);
return;
}
// case 4 叔叔为黑色
if (parent.isLeftChild() && x.isLeftChild()) { // LL
parent.color = Color.Black;
grandparent.color = Color.Red;
rightRotate(grandparent);
} else if (parent.isLeftChild() && !x.isLeftChild()) { // LR
leftRotate(parent);
x.color = Color.Black;
grandparent.color = Color.Red;
rightRotate(grandparent);
} else if (!parent.isLeftChild() && !x.isLeftChild()) { // RR
parent.color = Color.Black;
grandparent.color = Color.Red;
leftRotate(grandparent);
} else { // RL
rightRotate(parent);
x.color = Color.Black;
grandparent.color = Color.Red;
leftRotate(grandparent);
}
}
红黑树的遍历其实和之前的逻辑是一模一样的,这里就不做实现了,相信大家都会。其实树形结构的遍历都是一样的。
删除
删除节点其实也是和前面的大差不差,但是这里我们介绍一个新的删除方法叫做李代桃僵。
例如下面这颗红黑树假如我们要删除节点5,我们可以将节点五的key和value与它的后继节点(节点6)交换,接着我们只要删除节点6也就实现了删除节点五的目的。
另外在删除时如果遇到黑黑不平衡,我们需要进行调整,同样调整的策略前人也已经给我们总结好了,我们只需要根据策略实现代码就好了。
以下就是红黑树的删除代码,实现起来同样也是比较复杂的,处理好逻辑,情况一个一个处理相信你也是可以实现出来的。
// 删除
// 正常删除,会用到李代桃僵,遇到黑黑不平衡进行调整。
public void remove(int key) {
Node deleted = find(key);
if (deleted == null) {
return;
}
doRemove(deleted);
}
private void doRemove(Node deleted) {
Node replaced = findReplaced(deleted);
Node parent = deleted.parent;
// 没有孩子
if (replaced == null) {
// case 1 删除的是根节点
if (deleted == root) {
root = null;
} else {
if (isBlack(deleted)) {
// 复杂调整
fixDoubleBlack(deleted);
} else {
// 红色叶子,无需任何处理
}
if (deleted.isLeftChild()) {
parent.left = null;
} else {
parent.right = null;
}
deleted.parent = null;
}
return;
}
// 有一个孩子
if (deleted.left == null || deleted.right == null) {
// case 1 删除的是根节点
if (deleted == root) {
root.key = replaced.key;
root.value = replaced.value;
root.left = root.right = null;
} else {
if (deleted.isLeftChild()) {
parent.left = replaced;
} else {
parent.right = replaced;
}
replaced.parent = parent;
deleted.left = deleted.right = deleted.parent = null;
if (isBlack(deleted) && isBlack(replaced)) {
// 复杂调整
fixDoubleBlack(replaced);
} else {
// case 2
replaced.color = Color.Black;
}
}
return;
}
// case 0 有两个孩子 用李代桃僵 将其转换为有一个孩子或者没有孩子的情况
int tmpKey = deleted.key;
deleted.key = replaced.key;
replaced.key = tmpKey;
int tmpValue = deleted.value;
deleted.value = replaced.value;
replaced.value = tmpValue;
doRemove(replaced);
}
// 处理双黑(case3,case4,case5)
private void fixDoubleBlack(Node x) {
if (x == root) {
return;
}
Node parent = x.parent;
Node sibling = x.sibling();
// case 3 兄弟节点是红色
if (isRed(sibling)) {
if (x.isLeftChild()) {
leftRotate(parent);
} else {
rightRotate(parent);
}
parent.color = Color.Red;
sibling.color = Color.Black;
fixDoubleBlack(x);
return;
}
if (sibling != null) {
// case 4 兄弟是黑色,两个侄子也是黑色
if (isBlack(sibling.left) && isBlack(sibling.right)) {
sibling.color = Color.Red;
if (isRed(parent)) {
parent.color = Color.Black;
} else {
fixDoubleBlack(parent);
}
} else { // case 5 兄弟是黑色,侄子有红色
// LL
if (sibling.isLeftChild() && isRed(sibling.left)) {
rightRotate(parent);
sibling.left.color = Color.Black;
sibling.color = parent.color;
} else if (sibling.isLeftChild() && isRed(sibling.right)) {
// LR
sibling.right.color = parent.color;
leftRotate(sibling);
rightRotate(parent);
} else if (!sibling.isLeftChild() && isRed(sibling.left)) {
// RL
sibling.left.color = parent.color;
rightRotate(sibling);
leftRotate(parent);
} else {
// RR
leftRotate(parent);
sibling.right.color = Color.Black;
sibling.color = parent.color;
}
parent.color = Color.Black;
}
} else {
fixDoubleBlack(parent);
}
}
// 查找删除节点
private Node find(int key) {
Node p = root;
while (p != null) {
if (key < p.key) {
p = p.left;
} else if (key > p.key) {
p = p.right;
} else {
return p;
}
}
return null;
}
// 查找剩余节点
private Node findReplaced(Node deleted) {
if (deleted.left == null && deleted.right == null) {
return null;
}
if (deleted.left == null) {
return deleted.right;
}
if (deleted.right == null) {
return deleted.left;
}
Node s = deleted.right;
while (s.left != null) {
s = s.left;
}
return s;
}
其实至此我们已经完成了手撕红黑树这件事,相信看到这里的你真得好好奖励一下自己,非常不错,红黑树真的是比较男的数据结构了。
相关题目
红黑树这一块没有什么题目推荐,只要可以把红黑树给实现出来就很不错了,另外在后面的哈希表中我们还会再一次见到它。
如果不是天才,就请一步一步来。