2020-04-04,愿逝者安息,愿生者奋发,愿祖国昌盛,致敬英雄。
这篇文章整理一下比较器与二叉树、红黑树的知识点。
比较器问题的引出
如果要进行数组操作,最好要使用java.util.Arrays的操作类完成,因为这个类中提供有绝大部分的数组操作支持,同时在这个类中还提供有一种对象数组的排序支持:public static void sort(Object[] a);
public class TestDemo {
public static void main(String[] args) throws Exception {
Integer data[] = new Integer[] {2,8,9,10,68,94,36,8};
Arrays.sort(data);
System.out.println(Arrays.toString(data));
}
}
[2, 8, 8, 9, 10, 36, 68, 94]
同样,如果现在给定一个String型的对象数组,也可进行排序:
String data[] = new String[] {"s","e","g","r"};
[e, g, r, s]
Java.long.Integer和Java.long.String都是系统提供的程序类,如果说现在有一个自定义的类需要进行排序处理呢?
class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//setter、getter略掉
@Override
public String toString() {
return "Person [name=" + this.name + ", age=" + this.age + "]\n";
}
}
public class TestDemo {
public static void main(String[] args) throws Exception {
Person per [] = new Person[] {
new Person("A", 80),
new Person("B", 50),
new Person("C", 100)
};
Arrays.sort(per);
System.out.println(Arrays.toString(per));
}
}
这个程序没有语法错误,但是执行之后,就会报错:
Exception in thread "main" java.lang.ClassCastException: com.zijun.wang1.Person cannot be cast to java.base/java.lang.Comparable
at java.base/java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
at java.base/java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
at java.base/java.util.Arrays.sort(Arrays.java:1248)
at com.zijun.wang1.TestDemo.main(TestDemo.java:39)
首先我们会看到ClassCastException,这个错误说明有转型,说明我们不能转为java.lang.Comparable;这就说明任意的一个类,在默认情况下是无法使用系统内部的类来进行排序或比较的实现的。因为没有明确的指定出到底该如何进行比较的定义,也就是说没有比较规则。在Java中为了统一比较规则的定义,所以提供有比较器的接口:Comparable接口。
Comparable比较器
通过分析可以发现如果要实现对象的比较,肯定需要有比较器来制定比较的规则,而比较的规则就通过Comparable来实现,对于Comparable而言要清楚其基本的结构:
public interface Comparable<T> {
/**
* 实现对象的比较处理操作
* @param o 要比较的对象
* @return 当前的数据比传入的对象小返回负数,大于返回正数,相等返回0
*/
public int compareTo(T o);
}
class Person implements Comparable<Person>{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//setter、getter略掉
@Override
public String toString() {
return "Person [name=" + this.name + ", age=" + this.age + "]\n";
}
@Override
public int compareTo(Person per) {
return this.age - per.age;
}
}
public class TestDemo {
public static void main(String[] args) throws Exception {
Person per [] = new Person[] {
new Person("A", 80),
new Person("B", 50),
new Person("C", 100)
};
Arrays.sort(per);
System.out.println(Arrays.toString(per));
}
}
[Person [name=B, age=50]
, Person [name=A, age=80]
, Person [name=C, age=100]
]
我们只给Person类继承了Comparable接口,就实现了对自定义类的排序。这就说明,在排序里面只要有一个compareTo()方法进行排序规则的定义,而后整个Java系统中就可以为其实现排序处理了。
Comparator比较器
Comparator属于一种挽救的比较器支持,其目的是解决一些没有使用Comparable排序的类的对象数组排序操作。
用上面的Person类举个例子:
class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//setter、getter略掉
@Override
public String toString() {
return "Person [name=" + this.name + ", age=" + this.age + "]\n";
}
}
现在我们有一个Person类在定义的时候就没有排序的处理,但是经过了多次开发更新之后我们发现需要对其进行排序处理,但又不能去修改Person类,不能修改就意味着不能实现Comparable接口,这时候就要采用一种挽救的形式来实现比较,在Arrays类中,排序有另一种实现:
基于Comparator的排序处理:public static <T> void parallelSort(T[] a, Comparator<? super T> cmp);
在java.util.Comparator里面最初只定义有一个排序的compare()方法:public int compare(T o1, T o2);但是后来持续发展,在JDK1.8之后又出现了许多的static方法。
class PersonComparator implements Comparator<Person>{
@Override
public int compare(Person p1, Person p2) {
// TODO Auto-generated method stub
return p1.getAge() - p2.getAge();
}
}
在测试类进行排序处理的时候就可以利用排序规则实现操作:
public class TestDemo {
public static void main(String[] args) throws Exception {
Person per [] = new Person[] {
new Person("A", 80),
new Person("B", 50),
new Person("C", 100)
};
Arrays.sort(per, new PersonComparator());
System.out.println(Arrays.toString(per));
}
}
对于这种排序的操作,如果不是必须的情况下,强烈不建议使用Comparator接口,最好以Comparable为主。
解释一下Comparable和Comparator的区别:
- java.lang.Comparable是在类定义的时候实现的父接口,主要用于定义排序规则,只有一个compareTo的方法;
- java.util.Comparator是挽救的比较器操作,需要设置单独的比较器规则类实现排序,有compare方法,如果不是必须的情况下不建议使用这个类;
二叉树结构
在进行链表结构开发的过程中会发现,所有的数据按照首尾相连的状态进行保存,但要进行某一个数据查询的时候(判断该数据是否存在)所面对的时间按复杂度是“O(n)”,因为有多少个数据就要查找多少遍,如果现在数据量小的话,那么性能上是不会有太大的差别的,而一旦保存的数据量很大,这个时候时间复杂度就会严重损耗程序的运行性能(一直在遍历在遍历),那么现在对于数据的存储结构就要发生改变,应该尽可能的以减少检索次数为出发点来进行设计,对于现在的数据结构而言,最好的性能就是“O(log n)”,所以要想实现它,就要利用二叉树的结构来完成。
如果要想实现一颗树结构的定义,就要考虑数据的存储形式,在二叉树的实现中基本的实现原理如下:
- 取第一个数据为保存的根结点;
- 小于等于根节点的数据要放在结点的左子树;
- 大于的数据要放在结点的右子树;
我们以刚才的Person类来举例,按照年龄比较大小进行存放,随便写几个值,存放效果如下:
如果要进行数据检索的话,就要进行对每个结点的判断,但是他的判断是区分左右的,所以不会对整个的结构都进行判断处理,这样子时间复杂度就是“O(log n)”。
而对于二叉树而言,在进行数据获取的时候,有三种方式:前序遍历、中序遍历、后序遍历,现在只是以中序遍历为主,则上述的数据进行中序遍历,最终的结果就是:10、20、25、30、38、50、80、100,而这个结果就是二叉树中内容排序的结果。
二叉树基本实现
在实现二叉树的处理中,最为关键的就是数据的保存,而且由于数据牵扯到对象比较的问题,那么一定要有比较器的支持,而首选的比较器就是Comparable,我们来保存一个Person类的数据:
class Person implements Comparable<Person>{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//setter、getter略掉
@Override
public String toString() {
return "Person [name=" + this.name + ", age=" + this.age + "]\n";
}
@Override
public int compareTo(Person per) {
return this.age - per.age;
}
}
如果要想进行数据的保存,就要有一个结点类,结点类中由于牵扯到数据的保存问题,所以必须使用Comparable来区分大小。我们来简单的实现一下二叉树的处理:
/**
* TODO 实现二叉树的操作
* @param <T> 要进行二叉树的实现
*/
class BinaryTree<T extends Comparable<T>>{
private class Node{
private Comparable<T> data; //存放Comparable,可以比较大小也可以向下转型获取数据
private Node parent; //父结点
private Node left; //左子树
private Node right; //右子树
public Node(Comparable<T> data) { //构造方法直接负责数据的存储
this.data = data;
}
/**
* 实现结点数据的适当位置的存储
* @param newNode 创建新结点
*/
public void addNode(Node newNode) {
if(newNode.data.compareTo((T)this.data) <= 0) { //说明传进来的值比当前的值小
//存在左子树
if(this.left == null) { //没有左子树
this.left = newNode; //直接保存
newNode.parent = this; //保存父结点
}else { //当前结点的左子树有数据
//需要继续判断
this.left.addNode(newNode);
}
}else { //说明传进来的值比当前的值大
//存在右子树
if(this.right == null) { //没有右子树
this.right = newNode; //直接保存
newNode.parent = this; //保存父结点
}else { //当前结点的右子树有数据
//需要继续判断
this.right.addNode(newNode);
}
}
}
/**
* 实现所有数据的获取处理,按照中序遍历来完成
* 中序遍历:左-根-右
*/
public void toArrayNode() {
//首先找左子树
if(this.left != null) { //有左子树
this.left.toArrayNode(); //递归调用
}
//一直到当前结点没有左子树的时候,那么当前结点就是中根,直接保存
BinaryTree.this.returnData[BinaryTree.this.foot ++] = this.data;
//中跟找完后找右子树
if(this.right != null) { //有右子树
this.right.toArrayNode(); //递归调用
}
}
}
//**************** 二叉树功能实现***********************
private Node root; //明确表示,存放的根结点
private int count; //保存数据的个数
private Object[] returnData; //返回的数据
private int foot = 0; //脚标控制
/**
* 数据的保存
* @param data 要保存的数据内容
* @exception NullPointerException 保存数据为空时抛出的异常
*/
public void add(Comparable<T> data) {
if(data == null) {
throw new NullPointerException("保存的数据不允许为null");
}
//所有的数据本身不具备结点关系的匹配,所以要将其包装在Node类中
Node newNode = new Node(data); //保存结点
if(this.root == null) { //现在没有根结点
this.root = newNode; //第一个结点为根结点
}else { //如果有根结点,就要保存到一个合适的结点
this.root.addNode(newNode); //交给Node类去处理
}
this.count ++;
}
/**
* 一对象数组的形式返回数据,没有数据返回null
* @return 全部数据
*/
public Object[] toArray() {
if(this.count == 0) {
return null;
}
this.foot = 0; //脚标清零
this.returnData = new Object[this.count]; //保存长度为数组长度
this.root.toArrayNode(); //通过Node类处理
return this.returnData; // 返回全部的数据
}
}
class Person implements Comparable<Person>{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//setter、getter略掉
@Override
public String toString() {
return "Person [name=" + this.name + ", age=" + this.age + "]\n";
}
@Override
public int compareTo(Person per) {
return this.age - per.age;
}
}
public class TestDemo {
public static void main(String[] args) throws Exception {
BinaryTree<Person> bt = new BinaryTree<Person>();
bt.add(new Person("A", 50));
bt.add(new Person("B", 80));
bt.add(new Person("C", 30));
bt.add(new Person("D", 20));
bt.add(new Person("E", 10));
bt.add(new Person("F", 38));
bt.add(new Person("G", 100));
System.out.println(Arrays.toString(bt.toArray()));
}
}
[Person [name=E, age=10]
, Person [name=D, age=20]
, Person [name=C, age=30]
, Person [name=F, age=38]
, Person [name=A, age=50]
, Person [name=B, age=80]
, Person [name=G, age=100]
]
在进行数据添加的时候只是实现了结点关系的保存,而这种关系保存的后果就是所有的数据有序排列。
二叉树数据删除
二叉树之中的数据删除操作是非常复杂的,因为再删除的时候要考虑的情况是比较多的:
- 如果待删除节点没有子节点,那么直接删掉即可;
- 如果待删除节点只有一个子节点,那么直接删掉,并用其子节点去顶替它;
- 这个时候考虑两种情况:
- |-只有一个左子树:
- |-只有一个右子树:
- 如果待删除节点有两个子节点,这种情况比较复杂:首选找出它的后继节点,然后处理“后继节点"和“被删除节点的父节点”之间的关系,最后处理"后继节点的子节点”和“被删除节点的子节点"之间的关系;
- 简单来说,就是找到被删除结点的第一个右子树的左子树的...左子树的左叶子结点;
下面来使用代码实现一下,首先在Node类中增加新的功能:
/**
* 获取要删除的结点对象
* @param data 比较的对象
* @return 要删除的结点对象
*/
public Node getRemoveNode(Comparable<T> data) {
if(data.compareTo((T)this.data) == 0){
return this; //查找到了
}else if(data.compareTo((T)this.data) < 0) { //左边有数据
if(this.left != null) {
return this.left.getRemoveNode(data); //递归调用
}else {
return null;
}
}else { //右边有数据
if(this.right != null) {
return this.right.getRemoveNode(data); //递归调用
}else {
return null;
}
}
}
然后在BinaryTree中添加:
/**
* 实现删除操作
* @param data 要删除的数据
*/
public void remove(Comparable<T> data) {
//找到要删除的结点
Node removeNode = this.root.getRemoveNode(data);
if(removeNode != null) { //找到要删除的对象信息了
//第一种情况:没有子结点
if(removeNode.left == null && removeNode.right == null) {
if(removeNode.parent.left == removeNode) { //如果删除结点是左结点
removeNode.parent.left = null;
}else if(removeNode.parent.right == removeNode) { //如果删除结点是右结点
removeNode.parent.right = null;
}
removeNode.parent = null; //父结点直接断开引用
}else if(removeNode.left != null && removeNode.right == null) {
//只有一个左结点
removeNode.left.parent = removeNode.parent;
if(removeNode.parent.left == removeNode) { //如果删除结点是左结点
removeNode.parent.left = removeNode.left;
}else if(removeNode.parent.right == removeNode) { //如果删除结点是右结点
removeNode.parent.right = removeNode.left;
}
}else if(removeNode.left == null && removeNode.right != null) {
//只有一个右结点
removeNode.right.parent = removeNode.parent;
if(removeNode.parent.left == removeNode) { //如果删除结点是左结点
removeNode.parent.left = removeNode.right;
}else if(removeNode.parent.right == removeNode) { //如果删除结点是右结点
removeNode.parent.right = removeNode.right;
}
}else { //两边都有结点
//将右子树的最左叶子结点找到
Node moveNode = removeNode.right; //移动的结点
while(moveNode.left != null) { //还有左边的结点
moveNode = moveNode.left; //一直向左子树找
}//最后找到的就是删除结点的右子树的最左叶子结点
if(moveNode.parent.left == moveNode) { //如果找到结点是左结点
moveNode.parent.left = null; //断开原本的连接
moveNode.parent = removeNode.parent;
moveNode.right = removeNode.right;
moveNode.left = removeNode.left;
removeNode.left = null;
removeNode.right = null;
removeNode.parent = null;
}else if(moveNode.parent.right == moveNode) { //如果找到结点是右结点
moveNode.parent.right = null;//断开原本的连接
moveNode.parent = removeNode.parent;
moveNode.right = removeNode.right;
moveNode.left = removeNode.left;
removeNode.left.parent = moveNode;
if(removeNode.right != null) {
removeNode.right.parent = moveNode;
}
removeNode.left = null;
removeNode.right = null;
if(removeNode.parent.left == removeNode) { //如果删除结点是左结点
removeNode.parent.left = moveNode;
removeNode.parent = null;
}else if(removeNode.parent.right == removeNode) { //如果删除结点是右结点
removeNode.parent.right = moveNode;
removeNode.parent = null;
}
}
}
this.count --;
}
}
然后我们来试验一下:
public class TestDemo {
public static void main(String[] args) throws Exception {
BinaryTree<Person> bt = new BinaryTree<Person>();
bt.add(new Person("A", 80));
bt.add(new Person("B", 50));
bt.add(new Person("C", 90));
bt.add(new Person("D", 30));
bt.add(new Person("E", 60));
bt.add(new Person("F", 85));
bt.add(new Person("G", 95));
bt.add(new Person("H", 10));
bt.add(new Person("I", 55));
bt.add(new Person("J", 70));
System.out.println("【原来的数据】");
System.out.println(Arrays.toString(bt.toArray()));
bt.remove(new Person("I", 55));
System.out.println("【删除55后的数据】");
System.out.println(Arrays.toString(bt.toArray()));
bt.remove(new Person("D", 30));
System.out.println("【删除30后的数据】");
System.out.println(Arrays.toString(bt.toArray()));
bt.remove(new Person("C", 90));
System.out.println("【删除90后的数据】");
System.out.println(Arrays.toString(bt.toArray()));
}
}
【原来的数据】
[Person [name=H, age=10]
, Person [name=D, age=30]
, Person [name=B, age=50]
, Person [name=I, age=55]
, Person [name=E, age=60]
, Person [name=J, age=70]
, Person [name=A, age=80]
, Person [name=F, age=85]
, Person [name=C, age=90]
, Person [name=G, age=95]
]
【删除55后的数据】
[Person [name=H, age=10]
, Person [name=D, age=30]
, Person [name=B, age=50]
, Person [name=E, age=60]
, Person [name=J, age=70]
, Person [name=A, age=80]
, Person [name=F, age=85]
, Person [name=C, age=90]
, Person [name=G, age=95]
]
【删除30后的数据】
[Person [name=H, age=10]
, Person [name=B, age=50]
, Person [name=E, age=60]
, Person [name=J, age=70]
, Person [name=A, age=80]
, Person [name=F, age=85]
, Person [name=C, age=90]
, Person [name=G, age=95]
]
这里大家要注意一下,我是按照第三种情况,即有两个子结点的图中结点顺序来插入的,这段代码只能删除所标出的没有子结点或者一个子结点的情况,如果有两个子结点,包含情况有:
- 第一次删除50:找到55,替换50的位置;
- 第一次删除90:找到95,替换90的位置;
- 删除55之后删除50:找到70,替换50;
- 以及如果55有一个右叶子结点的情况、85有一个右叶子结点的情况;
- 这些情况分别要根据找到的替换结点(moveNode)和删除结点(removeNode)的不同位置来进行不同的条件判断;
- 还有删除根结点等等操作;
所以大家了解到删除操作的原理就可以了,如果不是必须的情况下,一般不会进行删除操作。
红黑树原理分析
通过整个的二叉树的实现相信大家已经清楚二叉树的主要特点:数据查询时可以提供更好的性能,但是这种原始的二叉树结构是有明显的缺陷的,当结构改变时(增加或删除操作)就有可能出现不平衡的问题:
这种二叉树如果出现不平衡的情况,就会又回到链表的操作,之前所解决的二叉树的性能问题最终又变成了null,也就是说如果要达到良好效果的二叉树,那么它首先应该是一个平衡二叉树,而且结点的层次深度应该相同:
如果所有的数据按照以上的结构进行保存,那么二叉树的检索操作执行效率一定是最高的,可是这样的树要忍受住频繁的增加或删除操作,所以针对于二叉树有了进一步的设计要求,也就是红黑树的要求。
红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色) , 同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为O(logn)。
红黑树是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树( symmetric binary B-trees )。后来,在1978年被Leo J.Guibas和Robert Sedgewick修改为如今的“红黑树”。
简单来说就是在Node结点中除了保存数据、父结点、左子结点、右子结点之外,还保存了一个Color:
enum Color{
RED,BLACK;
}
当然也可以用true和false来实现,不一定要实现枚举类:
private class Node{
private Comparable<T> data; //存放Comparable,可以比较大小也可以向下转型获取数据
private Node parent; //父结点
private Node left; //左子树
private Node right; //右子树
private Color color;
}
private class Node{
private Comparable<T> data; //存放Comparable,可以比较大小也可以向下转型获取数据
private Node parent; //父结点
private Node left; //左子树
private Node right; //右子树
private boolean color;
}
一个标准的红黑树结构如下:
红黑树特点
- 规则1:每个节点或者是黑色,或者是红色;
- 规则2:根节点必须是黑色;
- 规则3:每个叶子节点是黑色;
- |-Java实现的红黑树将使用null来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的;
- 规则4:如果一个节点是红色的,则它的子节点必须是黑色的;
- |-从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以连续的。若给定黑色节点的个数N,最短路径情况是连续的N个黑色,树的高度为N-1;最长路径的情况为节点红黑相间,树的高度为2(N- 1) ;
- 规则5:从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点数量;
- |-成为红黑树最主要的条件,后序的插入、删除操作都是为了遵守这个规定;
要注意几个点:
红色结点之后绝对不可能是红色结点,但没有说黑色结点之后不可以是黑色结点,也就是说可以黑-黑连接,但不允许红-红连接;主要是利用红色结点和黑色结点实现均衡控制,简单的举个例子,这个例子只是为了说明一下红黑的情况,不是原理:
简单点来说,红黑树的结构就是为了可以进行左旋和右旋的控制以保证树的平衡性,但是对于平衡性还需要考虑数据增加与删除的平衡,因为增加与删除的时候都是需要对这棵树进行平衡修复的。
数据插入原理
- 第一次插入,由于原树为空,所以只会违反红-黑树的规则2 ,所以只要把根节点涂黑即可;
- 如果插入节点的父节点是黑色的,那不会违背红-黑树的规则什么也不需要做;但是遇到如下三种情况时,就要开始变色和旋转了:
- |-插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的;
- |-插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点;
- |-插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;
在进行红黑树处理的时候为了方便都会把新的结点作为红色来描述,于是当设置根结点的时候就会违反根结点为黑色的规则,这个时候将根结点的颜色变黑即可;
对于三种情况,第一种:新结点的父结点和叔叔结点都为红色
将父结点和叔叔结点变为黑色,他们的父结点变为红色;
第二种:新结点的父结点是红色,叔叔结点是黑色,新结点是左子树结点
这时候首先将新结点的父结点与父结点的父结点互换位置与颜色,然后整个树做右旋处理。
第三种:父节点是红色,叔叔节点是黑色,新结点是右子树结点
首先将新结点进行左旋,这时候就和第二种情况一样了,这时候按照第二种情况处理就可以了。
红黑树的插入修复处理中,要根据当前结点和叔叔结点来进行修复处理。
数据删除平衡修复
刚才我们说过二叉树的删除处理操作:
- 如果待删除节点没有子节点,那么直接删掉即可;
- 如果待删除节点只有一个子节点,那么直接删掉,并用其子节点去顶替它;
- 如果待删除节点有两个子节点,这种情况比较复杂:首选找出它的后继节点,然后处理“后继节点”和"被删除节点的父节点"之间的关系,最后处理“后继节点的子节点”和“被删除节点的子节点”之间的关系。
现在来看一下红黑树的删除处理操作:
- 二叉树删除操作1后如果当前节点是黑色的根节点,那么不用任何操作,因为并没有破坏树的平衡性,即没有违背红黑树的规则;
- 如果当前节点是红色的,说明刚刚移走的后继节点是黑色的,那么不管后继节点的父节点是啥颜色,只要将当前节点涂黑就可以了,红-黑树的平衡性就可以恢复;
- 但是如果遇到以下四种情况,就需要通过变色或旋转来恢复红黑树的平衡了:
- |-当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的) ;
- |-当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的;
- |-当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点是黑色的;
- |-当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色;
第一种情况:当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的)
当前节点[30节点]是删除后重新保存的节点信息;
将当前节点[30节点]父节点[50节点]涂红,将兄弟节点[70节点]涂黑,然后将当前节点[30节点]的父节点[50节点]作为左旋支点;
第二种情况:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的
将当前节点[30节点]的兄弟节点[70节点]涂红,将当前节点[30节点]指向其父节点[50]节点,将当前节点[30节点]父节点[50节点]指向其祖父节点,不旋转。
第三种情况:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点是黑色的
将当前节点[30节点]的兄弟节点[70节点]涂红,把兄弟节点的左子节点[60节点]涂黑,然后以兄弟节点[70]节点作为支点进行右旋。
第四种情况:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色
将当前节点[30节点]的兄弟节点[70节点]涂成与父节点[50节点]相同的颜色,再把父节点[50节点]涂成黑色,把兄弟节点[70节点]的右子节点[80节点]涂黑,然后以当前节点的父节点[50节点]为支点进行左旋。
比较器和二叉树的关系很深,二叉树的操作离不开比较器,红黑树的原理大家可以了解一下,详细的知识可以先百度一下,我们下次见👋