数据结构之红黑树(图文并茂版、一篇足够版)

2023-11-01 05:20

本文主要是介绍数据结构之红黑树(图文并茂版、一篇足够版),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  • 为什么会出现红黑树
  • 红黑树的原理

网络上关于红黑树的资料很多,但是总是上来就给出红黑树的特点,弄的人云里雾里,索性自己学习整理,写一篇较为完整的红黑树文章,文章从上面二个标题来展开,首先介绍红黑树的由来。
红黑树动态插入演示

红黑树的由来

你应该听说过:二叉树、平衡二叉树、B树、B+树、红黑树。
说到底红黑树还是一种二叉树,是为了解决普通树存在的问题而诞生的
先来看二叉树

1.左子树上所有结点的值均小于或等于它的根结点的值。
2.右子树上所有结点的值均大于或等于它的根结点的值。
3.左、右子树也分别为二叉排序树。
在这里插入图片描述

二叉树的本质是进行二分查找,具有优越的查找性能。对于普通的二叉树,我们按顺序插入9、8、7、6、5、4、3、2、1会出现什么情况呢?

在这里插入图片描述

根据二叉树原理来插入,这棵树会显得很不平衡,其查找性能随之下降,这里我要查找3,几乎是线性遍历了。所以,在普通二叉树的基础上,我们还需要一定的理论,让树可以自己保持平衡,于是出现了AVL树,也就是平衡二叉树。平衡二叉树的理论可以让我们再插入图二的数据时,生成一个图一中的数据结构。AVL是一颗完美平衡的树,非常好非常理想化,但实际数据结构中我们仿佛听到的更多的是红黑树,为什么?
简而言之是,AVL具有完美的查找性能,但进行插入、删除时,需要进行的旋转操作次数不可控,有些效率问题。但红黑树保证的不是完美平衡,而是比较平衡,提高了插入、删除节点的效率,而查找性能并没有降低(红黑树log2N,AVL树logN),任何不平衡都会在三次旋转之内解决。所以实际工程中使用红黑树较多。

红黑树的原理

为了保证大致平衡这个理念,这才有了下面这几个到处都有的红黑树性质。

1. 根节点是黑色的;
2. 每个叶子节点都是黑色的空节点,也就是说,叶子节点不存储数据;
3. 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
4. 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;

为什么要有颜色区分?为了限制旋转次数。当颜色满足时就可停止旋转,当然节点的值也是影响旋转的因素之一。我们看下面这个例子:
插入3,此时3是黑色
在这里插入图片描述
插入1,此时1是红色
在这里插入图片描述
插入2,经过两次旋转,如下几步
第一步:
在这里插入图片描述
根据规则,插入的2是红色,不满足规则3
第二步:
在这里插入图片描述
不满足规则3
第三步:
在这里插入图片描述
不满足规则3,改变颜色
第四步:
在这里插入图片描述
这时我们继续插入4,会发生什么事情?

在这里插入图片描述
在这里插入图片描述
到这里基本是按照规则进行改变颜色,为了形象说明红黑树中颜色的作用,我们用AVL平衡树来做一个对照,在后面数据插入的过程中,会看到这个变化。
从001-004这个过程是一样的,用同样的办法构建一个AVL树:
在这里插入图片描述
接着看如下变化,插入节点5,在结构上看不出什么,但是平衡树进行的遍历次数要比红黑树多了。
在这里插入图片描述在这里插入图片描述
接着插入006
在这里插入图片描述在这里插入图片描述
经过对比,此时已经能看出变化,红黑树只是继续在005后追加006,而平衡树却经过遍历,进行了一次旋转。
具体的动画效果包含的信息也非常丰富

红黑树可视化
AVL树可视化
说真的这里不得不吐槽,我们国家的计算机教育怎么没有这么好的学习资源,这么好的学习资源甚至还没有普及,还很少人知道。

经过动画展示,以及可以看出来,红黑树的插入效率要比AVL树高,但不绝对平衡这件事。

下面我们看一下jdk1.8中,HashMap在处理hash冲突使用的红黑树源码。

红黑树源码

位置:HashMap.class 2214行开始
这里是红黑树的插入方法

        static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,TreeNode<K,V> x) {//新插入的节点为红色x.red = true;//定义变量和循环//xp:当前节点的父节点//xpp:爷爷节点//xppl:左叔叔节点,这里的变量名起的不好,应该是xpl更合适,下同//xppr:右叔叔节点for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {//如果当前节点父节点为空,直接返回插入的x,并置成黑色(false),说明白点就是插入第一个节点if ((xp = x.parent) == null) {x.red = false;return x;}//如果父节点不是红色或爷爷节点不为空,直接返回根节点。基本上是插入第二行和其他不需要变换的情况。请见下注释一说明情况。else if (!xp.red || (xpp = xp.parent) == null)return root;// 如果父节点是爷爷节点的左孩子Condition_1(和下面Condition_2对应),见注释二if (xp == (xppl = xpp.left)) {//Condition_1_1 如果其右叔叔不为空,且为红色,则改变右叔叔的颜色,改变父亲的颜色,改变爷爷的颜色,见注释二if ((xppr = xpp.right) != null && xppr.red) {xppr.red = false;	//叔叔节点置黑xp.red = false;		//父亲置黑xpp.red = true;		//爷爷置红x = xpp;			//进入下一次循环}//Condition_1_2 右叔叔为空 或者 为黑色else {// 如果当前节点是父节点的右孩子	Condition_1_2_1,见注释三if (x == xp.right) {// 父节点左旋root = rotateLeft(root, x = xp);// 见注释三xpp = (xp = x.parent) == null ? null : xp.parent;}// 如果父节点不为空 Condition_1_2_2 ,见注释三if (xp != null) {xp.red = false;if (xpp != null) {xpp.red = true;root = rotateRight(root, xpp);}}}}// 如果父节点是爷爷节点的右孩子Condition_2else {// 如果左叔叔是红色 Condition_2_1if (xppl != null && xppl.red) {xppl.red = false;// 左叔叔置为 黑色xp.red = false;// 父节点置为黑色xpp.red = true;// 爷爷置为红色x = xpp;// 运行到这里之后,就又会进行下一轮的循环了,将爷爷节点当做处理的起始节点}// 如果左叔叔为空或者是黑色Condition_2_2else {// 如果当前节点是个左孩子 Condition_2_2_1if (x == xp.left) {// 针对父节点做右旋root = rotateRight(root, x = xp);xpp = (xp = x.parent) == null ? null : xp.parent;}//Condition_2_2_2if (xp != null) {xp.red = false;if (xpp != null) {xpp.red = true;// 针对爷爷节点做左旋root = rotateLeft(root, xpp);}}}}}}

源码是左右互偶的,不太明白网上说红黑树最多旋转3次以内。通过阅读源码可以看出来,判断是否旋转的条件包括节点颜色、节点的左右性,节点是否为空三种条件。

上面我们有张图
在这里插入图片描述
这时如果插入007节点会发生什么事?
首先007是006的右子节点,且父节点为爷爷节点右孩子,直接进入Condition_2_2_2,一次旋转,父节点和爷爷节点变色即可。我们看结果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以上


注释一

条件一 !xp.red有这样一颗树,现在要插入一个大小为0.9的节点
在这里插入图片描述
在这里插入图片描述
我们会发现,当父节点为黑色,是可以直接插入,且不用旋转的。
条件二 (xpp = xp.parent) == null
初始树:
在这里插入图片描述
插入001
在这里插入图片描述
这种情况直接可以插入成功,不需要变换。所以可以直接返回root(该函数的返回值就是返回当前树的root节点)

注释二

有如下初始树
在这里插入图片描述
现在要插入004,则4的父亲005节点是其爷爷节点(006)的左孩子,满足该条件

xp == (xppl = xpp.left)

然后这里也满足第二个条件Condition_1_1

xppr = xpp.right) != null && xppr.red

现在插入004,按照代码里的规则,会改变007的颜色为黑,005的颜色黑,006的颜色为红。我们看动画分解:
在这里插入图片描述
在这里插入图片描述
完全按照代码的逻辑实现的,但是真里有人问了,006根节点不应该是黑色么,注意看这里面的代码是一个for循环,改变颜色后并没有return,而是进入下一次循环,第二次就会将爷爷节点当作当前处理的节点来处理,也就是006,然后就会执行注释一的逻辑,将其置黑,并return。

if ((xp = x.parent) == null) {x.red = false;return x;
}

注释三

Condition_1_2_1 如果当前节点是父节点的右孩子
初始化树
在这里插入图片描述
现在要插入003,003的位置会在002的右侧(永远往叶节点插入数据,然后再旋转)
在这里插入图片描述
条件满足了,这时候父节点左旋
在这里插入图片描述
进入下一个条件,此时002满足Condition_1_2_2,
在这里插入图片描述
将父节点置黑,爷爷节点置红,爷爷节点右旋转
在这里插入图片描述

这篇关于数据结构之红黑树(图文并茂版、一篇足够版)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/320780

相关文章

C#数据结构之字符串(string)详解

《C#数据结构之字符串(string)详解》:本文主要介绍C#数据结构之字符串(string),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录转义字符序列字符串的创建字符串的声明null字符串与空字符串重复单字符字符串的构造字符串的属性和常用方法属性常用方法总结摘

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

【408数据结构】散列 (哈希)知识点集合复习考点题目

苏泽  “弃工从研”的路上很孤独,于是我记下了些许笔记相伴,希望能够帮助到大家    知识点 1. 散列查找 散列查找是一种高效的查找方法,它通过散列函数将关键字映射到数组的一个位置,从而实现快速查找。这种方法的时间复杂度平均为(

浙大数据结构:树的定义与操作

四种遍历 #include<iostream>#include<queue>using namespace std;typedef struct treenode *BinTree;typedef BinTree position;typedef int ElementType;struct treenode{ElementType data;BinTree left;BinTre

Python 内置的一些数据结构

文章目录 1. 列表 (List)2. 元组 (Tuple)3. 字典 (Dictionary)4. 集合 (Set)5. 字符串 (String) Python 提供了几种内置的数据结构来存储和操作数据,每种都有其独特的特点和用途。下面是一些常用的数据结构及其简要说明: 1. 列表 (List) 列表是一种可变的有序集合,可以存放任意类型的数据。列表中的元素可以通过索

CSP-J基础之数学基础 初等数论 一篇搞懂(一)

文章目录 前言声明初等数论是什么初等数论历史1. **古代时期**2. **中世纪时期**3. **文艺复兴与近代**4. **现代时期** 整数的整除性约数什么样的整数除什么样的整数才能得到整数?条件:举例说明:一般化: 判断两个数能否被整除 因数与倍数质数与复合数使用开根号法判定质数哥德巴赫猜想最大公因数与辗转相除法计算最大公因数的常用方法:举几个例子:例子 1: 计算 12 和 18

浙大数据结构:04-树7 二叉搜索树的操作集

这道题答案都在PPT上,所以先学会再写的话并不难。 1、BinTree Insert( BinTree BST, ElementType X ) 递归实现,小就进左子树,大就进右子树。 为空就新建结点插入。 BinTree Insert( BinTree BST, ElementType X ){if(!BST){BST=(BinTree)malloc(sizeof(struct TNo