二叉树系列:红黑树

article/2025/10/1 6:10:27

介绍

红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树。
红黑树是特殊的二叉查找树,意味着它满足二叉查找树的特征:任意一个节点所包含的键值,大于等于左孩子的键值,小于等于右孩子的键值。
除了具备该特性之外,红黑树还包括许多额外的信息。

红黑树的每个节点上都有存储位表示节点的颜色,颜色是红(Red)或黑(Black)。
红黑树的特性:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

关于它的特性,需要注意的是:
第一,特性(3)中的叶子节点,是只为空(NIL或null)的节点。
第二,特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

二 红黑树操作

常见的操作就是插入跟删除。因为红黑树的特性插入、删除会导致破坏了平衡性。所以需要修正。常见的修正操作就是颜色设置及旋转。使它重新成为红黑树。

写贴一下treemap的实现

static final class Entry<K,V> implements Map.Entry<K,V> {K key;V value;Entry<K,V> left;Entry<K,V> right;Entry<K,V> parent;boolean color = BLACK;/*** Make a new cell with given key, value, and parent, and with* {@code null} child links, and BLACK color.*/Entry(K key, V value, Entry<K,V> parent) {this.key = key;this.value = value;this.parent = parent;}
...

1 左旋

对x进行左旋,意味着"将x变成一个左节点"。


/** From CLR */private void rotateLeft(Entry<K,V> p) {if (p != null) {//设置p的右孩子节点为rEntry<K,V> r = p.right;// 将 “r的左孩子” 设为 “p的右孩子”;p.right = r.left;//如果r的左孩子不为空,则将p设置为“r左孩子的父亲”if (r.left != null)r.left.parent = p;//把p的父亲设置为r的父亲    r.parent = p.parent;//如果p的父亲为空,则r设为根节点if (p.parent == null)root = r;//p是它父节点的左孩子,则设置r是“p父节点的左孩子”else if (p.parent.left == p)p.parent.left = r;//p是它父节点的右孩子,则设置r是“p父节点的右孩子”elsep.parent.right = r;//p设置为r的左孩子r.left = p;//p的父节点设为rp.parent = r;}}

2 右旋


对y进行右旋,意味着"将y变成一个右节点"。思路同上,不再逐一注释

 /** From CLR */private void rotateRight(Entry<K,V> p) {if (p != null) {Entry<K,V> l = p.left;p.left = l.right;if (l.right != null) l.right.parent = p;l.parent = p.parent;if (p.parent == null)root = l;else if (p.parent.right == p)p.parent.right = l;else p.parent.left = l;l.right = p;p.parent = l;}}

3 插入

将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过"旋转和重新着色"等一系列操作来修正该树,使之重新成为一颗红黑树。详细描述如下:
第一步: 将红黑树当作一颗二叉查找树,将节点插入。
       红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!

第二步:将插入的节点着色为"红色"。

第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
       第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
       对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。
       对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
       对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
       对于"特性(4)",是有可能违背的!

       那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。

下面是treemap的实现方式

public V put(K key, V value) { Entry<K,V> t = root; // 一个空链表if (t == null) { root = new Entry<K,V>(key, value, null); size = 1; // 记录修改次数为 1 modCount++; return null; } int cmp; Entry<K,V> parent; Comparator<? super K> cpr = comparator; // 采用定制排序if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; do {  parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } // 将新插入的节点作为 parent 节点的子节点Entry<K,V> e = new Entry<K,V>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; // 修复红黑树fixAfterInsertion(e);                               size++; modCount++; return null; }
private void fixAfterInsertion(Entry<K,V> x) { x.color = RED; //设新插入的为红色,对应第二步// 直到 x 节点的父节点不是根,且 x 的父节点不是红色while (x != null && x != root && x.parent.color == RED) { //  x 的父节点是其父节点的左子节点if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 获取 x 的父节点的兄弟节点Entry<K,V> y = rightOf(parentOf(parentOf(x))); // 如果 x 的父节点的兄弟节点是红色(需要颜色翻转)if (colorOf(y) == RED) { // 将 x 的父节点设为黑色setColor(parentOf(x), BLACK); // 将 x 的父节点的兄弟节点设为黑色setColor(y, BLACK); // 将 x 的父节点的父节点设为红色setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } // 如果 x 的父节点的兄弟节点是黑色(需要旋转)else { // 如果 x 是其父节点的右子节点if (x == rightOf(parentOf(x))) { // 将 x 的父节点设为 x x = parentOf(x); rotateLeft(x); } // 把 x 的父节点设为黑色setColor(parentOf(x), BLACK); // 把 x 的父节点的父节点设为红色setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } // 以下的操作是上面操作的对称,就不做分析了else { Entry<K,V> y = leftOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == leftOf(parentOf(x))) { x = parentOf(x); rotateRight(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } // 将根节点设为黑色root.color = BLACK; }
其中的parentof是获取父节点。这里需要结合别的资料来展开这么多判断场景。我也忘了。

4 删除

之前二叉树就删除是个复杂操作,因为删除对应不同的情况。

红黑树也不例外:

将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:
第一步:将红黑树当作一颗二叉查找树,将节点删除。
       这和"删除常规二叉查找树中删除节点的方法是一样的"。分3种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。我们队红黑树删除的操作,每一件事情都归咎于能够删除树叶。这是因为要删除带有两个孩子节点的可以用右子树的最小节点代替它

第二步:通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
        因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。

    /*** Delete node p, and then rebalance the tree.*/private void deleteEntry(Entry<K,V> p) {modCount++;size--;// If strictly internal, copy successor's element to p and then make p// point to successor.if (p.left != null && p.right != null) {// 用 p 节点的中序后继节点代替 p 节点Entry<K,V> s = successor(p);p.key = s.key;p.value = s.value;p = s;} // p has 2 children// Start fixup at replacement node, if it exists.Entry<K,V> replacement = (p.left != null ? p.left : p.right);if (replacement != null) {// Link replacement to parentreplacement.parent = p.parent;if (p.parent == null)root = replacement;else if (p == p.parent.left)p.parent.left  = replacement;elsep.parent.right = replacement;// Null out links so they are OK to use by fixAfterDeletion.p.left = p.right = p.parent = null;// Fix replacementif (p.color == BLACK)fixAfterDeletion(replacement);//修复红黑树} else if (p.parent == null) { // return if we are the only node.root = null;} else { //  No children. Use self as phantom replacement and unlink. 代替节点为空的情况if (p.color == BLACK)fixAfterDeletion(p);if (p.parent != null) {if (p == p.parent.left)p.parent.left = null;else if (p == p.parent.right)p.parent.right = null;p.parent = null;}}}

下面是修复方法。修复的过程中分为四种情况:
1、N的兄弟节点W为红色
2、N的兄弟w是黑色的,且w的俩个孩子都是黑色的。
3、N的兄弟w是黑色的,w的左孩子是红色,w的右孩子是黑色。
4、N的兄弟w是黑色的,且w的右孩子时红色的。

/ 删除节点后修复红黑树private void fixAfterDeletion(Entry<K,V> x) { // 直到 x 不是根节点,且 x 的颜色是黑色while (x != root && colorOf(x) == BLACK) { // 如果 x 是其父节点的左子节点if (x == leftOf(parentOf(x))) { // 获取 x 节点的兄弟节点Entry<K,V> sib = rightOf(parentOf(x)); // 如果 sib 节点是红色if (colorOf(sib) == RED) { // 将 sib 节点设为黑色setColor(sib, BLACK); // 将 x 的父节点设为红色setColor(parentOf(x), RED); rotateLeft(parentOf(x)); // 再次将 sib 设为 x 的父节点的右子节点sib = rightOf(parentOf(x)); } // 如果 sib 的两个子节点都是黑色if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { // 将 sib 设为红色setColor(sib, RED); // 让 x 等于 x 的父节点x = parentOf(x); } else { // 如果 sib 的只有右子节点是黑色if (colorOf(rightOf(sib)) == BLACK) { // 将 sib 的左子节点也设为黑色setColor(leftOf(sib), BLACK); // 将 sib 设为红色setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } // 设置 sib 的颜色与 x 的父节点的颜色相同setColor(sib, colorOf(parentOf(x))); // 将 x 的父节点设为黑色setColor(parentOf(x), BLACK); // 将 sib 的右子节点设为黑色setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } // 下面是上面的对称操作就不做具体分析了else { Entry<K,V> sib = leftOf(parentOf(x)); if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateRight(parentOf(x)); sib = leftOf(parentOf(x)); } if (colorOf(rightOf(sib)) == BLACK && colorOf(leftOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { if (colorOf(leftOf(sib)) == BLACK) { setColor(rightOf(sib), BLACK); setColor(sib, RED); rotateLeft(sib); sib = leftOf(parentOf(x)); } setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(leftOf(sib), BLACK); rotateRight(parentOf(x)); x = root; } } } setColor(x, BLACK); }

Treemap里面还有很多其他公用方法,下面是测试结果:

package com.daojia.collect;import java.util.Iterator;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;public class TreeMapTest {private static final int a[] = {10, 40, 30, 60, 90, 70, 20, 50, 80};public static void main(String[] args) {int i, ilen = a.length;TreeMap tree=new TreeMap();System.out.printf("== 原始数据: ");for(i=0; i<ilen; i++)System.out.printf("%d ", a[i]);System.out.printf("\n");for(i=0; i<ilen; i++) {tree.put(a[i],a[i]);		        System.out.printf("== 添加节点: %d\n", a[i]);System.out.printf("== 树的详细信息: \n");		              System.out.printf(  tree.toString()+"\n");}System.out.printf("遍历: ");		      Set<Integer> keys = tree.keySet();for (Integer key : keys){System.out.println(key);}System.out.printf("== 最小值: %s\n", tree.firstEntry().getValue());System.out.printf("== 最大值: %s\n", tree.lastEntry().getValue());tree.remove(a[5]);System.out.printf("== 删除节点: %d\n", a[5]);System.out.printf("== 树的详细信息: \n");System.out.printf(   tree.toString()+"\n");// 销毁二叉树NavigableMap  nmap =tree.descendingMap();Iterator iterator = nmap.keySet().iterator();while (iterator.hasNext()) {Integer key = (Integer) iterator.next();// 获取ValueSystem.out.println(tree.get(key));}}}
== 原始数据: 10 40 30 60 90 70 20 50 80 
== 添加节点: 10
== 树的详细信息: 
{10=10}
== 添加节点: 40
== 树的详细信息: 
{10=10, 40=40}
== 添加节点: 30
== 树的详细信息: 
{10=10, 30=30, 40=40}
== 添加节点: 60
== 树的详细信息: 
{10=10, 30=30, 40=40, 60=60}
== 添加节点: 90
== 树的详细信息: 
{10=10, 30=30, 40=40, 60=60, 90=90}
== 添加节点: 70
== 树的详细信息: 
{10=10, 30=30, 40=40, 60=60, 70=70, 90=90}
== 添加节点: 20
== 树的详细信息: 
{10=10, 20=20, 30=30, 40=40, 60=60, 70=70, 90=90}
== 添加节点: 50
== 树的详细信息: 
{10=10, 20=20, 30=30, 40=40, 50=50, 60=60, 70=70, 90=90}
== 添加节点: 80
== 树的详细信息: 
{10=10, 20=20, 30=30, 40=40, 50=50, 60=60, 70=70, 80=80, 90=90}
遍历: 10
20
30
40
50
60
70
80
90
== 最小值: 10
== 最大值: 90
== 删除节点: 70
== 树的详细信息: 
{10=10, 20=20, 30=30, 40=40, 50=50, 60=60, 80=80, 90=90}
90
80
60
50
40
30
20
10


参考:

https://www.cnblogs.com/skywang12345/p/3624343.html


http://chatgpt.dhexx.cn/article/CxcAr1Z9.shtml

相关文章

红黑树结构原理的图文讲解(非代码)

1.引言 HashMap的基本结构是数组&#xff0c;链表和红黑树。以数组为基本形态&#xff0c;数组中的元素先以链表形式储存&#xff0c;当链表的长度超过8时&#xff08;包含数组上的那个链表头&#xff09;就会将链表转换为红黑树&#xff0c;以加快修改和查询效率。当然除了Ha…

二叉树与红黑树见解

目录 一、红黑树简介二、 红黑树的特性三、红黑数的应用四、红黑树的原理实现4.1 识别红黑树4.2 红黑树节点的旋转4.3 插入节点4.3.1分情况讨论&#xff1a;4.3.2 代码示例 4.4删除节点相关引用 一、红黑树简介 红黑树是一种自平衡的二叉查找树&#xff0c;是一种高效的查找树…

什么是红黑树(内存最优的二叉树)

一.红黑树定义 红黑树(Red Black Tree) 是一种自平衡二叉查找树&#xff0c;是在计算机科学中用到的一种数据结构&#xff0c;典型的用途是实现关联数组。 它是在1972年由Rudolf Bayer发明的&#xff0c;当时被称为平衡二叉B树(symmetric binary B-trees)。后来&#xff0c;在1…

【二叉树进阶】红黑树(Red Black Tree) - 平衡二叉搜索树

文章目录 一、红黑树的概念二、红黑树的性质2.1 红黑树和AVL树效率对比 三、红黑树的结构&#xff08;KV模型&#xff09;四、红黑树的插入4.1 插入节点4.2 平衡化操作&#xff08;难点&#xff09;4.2.1 情况一4.2.2 情况二4.2.3 情况三 4.3 总结 五、红黑树的验证六、红黑树的…

MySQL 慢查询日志导入 Elasticsearch 可视化查询分析

当应用程序后台 SQL 查询慢的时候我们一般第一时间会查看数据库慢查询记录&#xff0c;但是慢查询记录是原始文本&#xff0c;直接查询搜索分析比较费时费力&#xff0c;虽然业界有针对 MySQL 慢查询分析的命令行工具&#xff08;比如&#xff1a;pt-query-digest&#xff09;&…

MySQL 慢查询日志如何查看及配置

简介 MySQL 慢查询日志是排查问题 SQL 语句&#xff0c;以及检查当前 MySQL 性能的一个重要功能。 查看是否开启慢查询功能&#xff1a; 说明&#xff1a; slow_query_log 慢查询开启状态 slow_query_log_file 慢查询日志存放的位置&#xff08;这个目录需要MySQL的运行帐号的…

MySQL高级篇——聊聊MySQL的慢查询日志

文章目录&#xff1a; 1.数据库服务器的优化步骤 2.查看系统性能参数 3.定位执行慢的 SQL&#xff1a;慢查询日志 4.查看 SQL 执行成本&#xff1a;SHOW PROFILE 1.数据库服务器的优化步骤 当我们遇到数据库调优问题的时候&#xff0c;该如何思考呢&#xff1f;这里把思考…

清理mysql慢查询日志_MySQL清理慢查询日志slow_log的方法

一、清除原因 因为之前打开了慢查询,导致此表越来越大达到47G,导致磁盘快被占满,使用xtrabackup进行备份的时候文件也超大。 mysql> show variables like log_output%; Connection id: 1694401091 Current database: mysql +---------------+-------+ | Variable_name | …

mysql 慢查询日志的设置与优化

目录 1 引言 2 慢查询日志配置 3 分析工具 1 引言 MySQL数据中有记录慢查询的一种手段。并且是MySQL自带的。可用来排查那些查询sql语句执行得慢。从而给开发者提供一个调优得依据。 MySQL 慢查询的相关参数解释&#xff1a; slow_query_log &#xff1a;是否开启慢查…

如何开启mysql慢查询日志?

1、查看mysql的慢查询日志是否开启 show variables like %query%; 可以看到slow_query_log的值是OFF&#xff0c;也就是mysql默认是不启用慢查询日志的。 这里还有个long_query_time&#xff0c;默认是10秒&#xff0c;也就是超过了10秒即为慢查询。 log_queries_not_using_…

MySQL慢查询日志详解

本次代码执行环境的mysql版本是 &#xff1a;5.6.37-log 1.慢查询日志概念(也叫慢日志):在 MySQL 中执行时间超过指定时间的 SQL 语句 2.常见的几个相关的变量 (可以直接去mysql下的配置文件my.cnf文件中去改,我下面是直接在SQLyog中进行操作) 默认情况下慢查询日志是关闭的…

MySQL 慢查询日志 使用方法浅析 日志定位与优化技巧

目录 前言 1、如何开启使用慢查询日志&#xff1f; 1.1 开启慢查询日志 1.2 设置慢查询阈值 1.3 确定慢查询日志的文件名和路径 1.3.1 查询MySQL数据目录 1.3.2 查询慢查询日志文件名 1.3.3 查询全局设置变量 1.3.4 查询单个变量命令 1.3.5 其他注意事项 2、如何定位并优…

mysql慢查询日志在哪里

如何查找MySQL中查询慢的SQL语句 你是指慢查询日志吗&#xff1f; 在my.ini中加上下面两句话 log-slow-queriese:\mysql5.5\mysql_slow_query.log long_query_time10 前面一句是设置慢查询日志存放路径&#xff0c;第二句是指多少秒以上算慢查询&#xff0c;上面的语句&#xf…

MySQL慢查询日志分析

&#xff08;1&#xff09;慢查询日志 MySQL提供了慢SQL的日志记录功能&#xff0c;我们可以通过设置一些属性来记录系统使用过程中慢查询的执行日志。使用MySQL慢查询日志对有效率问题的SQL进行监控。 查看属性 【1】查看MySQL是否开启慢查询日志记录功能 show variables l…

MySQL优化之慢日志查询

目录 一、慢查询日志(slow_query_log)概念二、慢查询日志实践1. 打开慢查询日志开关2. 设置合理的、业务可以接受的慢查询时间上限long_query_time3. 压测执行各种业务4. 查看慢查询日志5. 用explain分析这些耗时的sql语句&#xff0c;从而针对性优化 三、show profiles查看sql…

MySQL日志(一)—— 慢查询日志slow log

一、慢查询日志&#xff08;slow log&#xff09; 慢查询日志&#xff0c;就是查询超过一定的时间没有返回结果的时候&#xff0c;MySQL会将执行的SQL记录到日志中&#xff0c;这个日志&#xff0c;就称为慢查询日志。通过分析慢查询日志&#xff0c;可以快速找出执行慢的SQL语…

【OpenCv3】 VS C++ (五):SLIC超像素分割算法

下一节地址&#xff1a;https://blog.csdn.net/qq_40515692/article/details/102788157 OpenCv专栏&#xff1a;https://blog.csdn.net/qq_40515692/article/details/102885061 超像素&#xff08;SuperPixel&#xff09;&#xff0c;就是把原本多个像素点&#xff0c;组合成一…

superpixels(超像素)

superpixels(超像素&#xff09; 1.理解&#xff1a; 超像素不是在普通的像素基础上继续微观细分&#xff0c;超像素是一系列像素的集合&#xff0c;这些像素具有类似的颜色、纹理等特征&#xff0c;距离也比较近。其中超像素比较常用的一种方法是SLIC Semantic Segmentatio…

Seeds超像素分割

#%% SEED超像素分割 import cv2 import numpy as np import imageio # print(dir(cv2.ximgproc))img imageio.imread(rE:\Vaihingen\data\orginalimages\top_mosaic_09cm_area31.tif)[:,:,::-1] converted_img cv2.cvtColor(img, cv2.COLOR_BGR2HSV)# print(type(img_feature…

Python实现超像素分割

目录 一、什么是超像素&#xff1f;二、超像素具有哪些特点&#xff1f;三、Simple Linear Iterative Clustering (SLIC)算法实现步骤四、SLIC算法代码实现五、效果展示和分析六、基于超像素的边缘检测代码七、基于超像素的边缘检测效果展示与分析八、思维扩展参考资料注意事项…