【数据结构】如何创建一棵红黑树(附动图讲解)

2024-05-03 20:44

本文主要是介绍【数据结构】如何创建一棵红黑树(附动图讲解),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、前言

二、红黑树的概念

三、红黑树的性质

四、红黑树节点的定义

五、红黑树的插入

5.1 节点的初始颜色 

5.2 红黑树的调整

六、红黑树的验证

6.1 验证有序

6.2 验证红黑树性质

七、红黑树与AVL树的比较


一、前言

在前面AVL树的学习中,我们知道了如何通过对平衡因子的调整、判断和旋转得到一棵严格平衡的二叉搜索树,虽然AVL树能够降低搜索树的高度,加快搜索效率,但是频繁的旋转导致创建一棵AVL树的代价并不小,因此,红黑树诞生了。


二、红黑树的概念

红黑树是一种自平衡的二叉搜索树,相对于AVL树的严格平衡,它遵循一种相对平衡,即最长路径不超过最短路径的二倍。红黑树由 Rudolf Bayer 于1972年发明,在当时被称为对称二叉B树(symmetric binary B-trees)。后来在1978年被 Leo J.Guibas 和 Robert Sedgewick 修改为如今的红黑树。

红黑树的应用十分广泛,它能够在O(logN)的时间复杂度内完成搜索、查找和删除操作,后面要学习的C++容器map和set,其底层就是用红黑树实现的


三、红黑树的性质

红黑树之所以叫这个名字,是因为其每个节点中都增加了一个颜色变量,该变量不是红色就是黑色

红黑树的以下几个性质,是必须严格遵守的,否则就不能被称为红黑树: 

  • 性质1:每个节点不是红色就是黑色
  • 性质2:根节点必须是黑色的

这里的根节点不包括子树的根节点,而是整棵树唯一的_root节点

所以红黑树的左右子树不是红黑树(区分:AVL树的左右子树也是AVL树) 

  • 性质3:可以存在两个或多个连续的黑色节点,但是不能存在连续的红色节点
  • 性质4:每条路径上的黑色节点数量必须相同

性质2、3和性质4就决定了红黑树的最长路径不超过最短路径的二倍,例如:

图中的红黑树已省略其他节点,只保留最长路径和最短路径

可以看到此时路径中的黑色节点数量相同,因此无法在最长路径中再添加一个黑色节点;又因为不能存在连续的红色节点,而最长路径中的最后一个节点为红色,因此也无法在最长路径中添加红色节点。

通过观察可以发现,在红黑树中全黑的路径必然是最短路径,而一黑一红交替的路径是最长路径

  • 性质5:红黑树的空节点(NIL节点)默认为黑色


四、红黑树节点的定义

红黑树的节点与AVL树的节点大致相同,只是没有了平衡因子,取而代之的是颜色

每个新节点的初始颜色都设置为红色,原因会在后面的插入操作中讲 

我们可以用一个枚举来列举颜色

enum Colour
{RED,BLACK
};template<class T>
struct RBTreeNode
{RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;T _data;Colour _col;RBTreeNode(const T& data):_left(nullptr), _right(nullptr), _parent(nullptr), _data(data), _col(RED) //初始颜色为红色{}
};


五、红黑树的插入

红黑树本质上也是一棵二叉搜索树,因此在插入节点时也需要遵循二叉搜索树的规则。

5.1 节点的初始颜色 

在插入节点时,我们遇到的第一个问题是:为什么节点的初始颜色是红色?不能是黑色吗?

假设我们把节点的初始颜色设置为黑色,那么在我们插入一个新节点的时候会发生什么事呢?

例如:

可以发现,在插入新节点(黑)的时候,该路径的黑色节点数目发生了变化,破坏了性质4,即每条路径的黑色节点数目必须相同。

当红黑树的性质被破坏后,我们需要对其进行旋转或变色等操作使其重新成为一棵红黑树

但是因为此时性质4被破坏,我们就需要将每条路径的黑色节点数量调整到相同,同时又不能破坏其他的性质,这个过程要付出的代价是非常非常大的!

但是如果节点的初始颜色是红色,就分为两种情况:

(1)插入新节点,此时新节点的父节点为黑色

例如:

这是最理想的情况,因为此时插入一个新节点没有破坏任何一个性质,所以不需要进行调整。

(2)插入新节点,此时新节点的父节点为红色

例如:

插入新节点后,出现了连续的红色节点,性质3被破坏,需要进行调整。

可以看出,如果我们将节点的初始颜色设置为红色,在某些情况下是不需要进行调整的

而当我们检测到插入的新节点的父节点是红色时,才需要进行调整,调整的过程也很简单,接下来我们就会讲到。

至此,我们可以写出不包括调整部分的插入函数的代码了:

bool insert(const pair<const K, V> &kv)
{if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}Node *parent = nullptr;Node *cur = _root;while (cur){parent = cur;if (kv.first > cur->_kv.first)cur = cur->_right;else if (kv.first < cur->_kv.first)cur = cur->_left;elsereturn false;}cur = new Node(kv);cur->_col = RED; // 新节点默认为红色if (kv.first > parent->_kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}//调整部分_root->_col = BLACK; // 无论如何,都将根变为黑色return true;
}

5.2 红黑树的调整(附动图)

当插入的新节点的父节点是红色时,就出现了连续的红色节点,此时我们需要对红黑树进行调整

调整的时候,又分为三个情况:

(1)插入新节点,父节点为红,叔叔节点存在且也为红

这种情况是最好处理的,将父节点和叔叔节点变成黑色,再将祖父节点变为红色,这样既恢复了性质3,又不会破坏性质4

需要注意的是,祖父节点不一定为根节点,其父节点可能也为红色,所以需要继续向上调整。

(2)插入新节点,父节点为红,叔叔节点不存在或存在且为黑,父节点和祖父节点的相对位置与新节点与父节点的相对位置相同

遇到这种情况,向上面这种单纯的变色已经不足以解决问题了,需要进行一次单旋后再变色

当需要旋转时,对红黑树的旋转方式和AVL树的旋转方式类似。 

这里我们用叔叔节点存在且为黑的情况来进行演示:

在调整的时候,我们需要时刻遵循每条路径的黑色节点数量相同的原则,因此a、b、c中的黑色节点必定比d、e中的黑色节点数目多1个(因为叔叔节点为黑色,已经为d、e中的路径提供了一个黑色节点) 

上面的情况,当父节点在祖父节点的左边且新节点在父节点的左边时,先进行右单旋再变色

但是如果当父节点在祖父节点的右边且新节点在父节点的右边时,则先进行左单旋再变色

当叔叔节点不存在时,处理方法和叔叔节点存在且为黑的相同。

(3)插入新节点,父节点为红,叔叔节点不存在或存在且为黑,父节点和祖父节点的相对位置与新节点与父节点的相对位置相反

这种情况,祖父节点-父节点-新节点的连线之间会有一个明显的折返角度,也就是说当父节点在祖父节点的左边时,新节点在父节点的右边;而当父节点在祖父节点的右边时,新节点在父节点的左边

和AVL树的旋转类似,这种情况下需要进行两次单旋+变色

这里我们还是用叔叔节点存在且为黑的情况来进行演示:

(动图里的双旋有误,应改成两次单旋)

上面的情况,当父节点在祖父节点的左边且新节点在父节点的右边时,先进行左单旋再进行右单旋,最后变色

如果当父节点在祖父节点的右边且新节点在父节点的左边时,则先进行右单旋再进行左单旋,最后变色

当叔叔节点不存在时,处理方法和叔叔节点存在且为黑的相同。

当我们清楚了3种需要进行调整的情况后,就可以编写完整的插入函数代码了:

bool insert(const pair<const K, V> &kv)
{if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}Node *parent = nullptr;Node *cur = _root;while (cur){parent = cur;if (kv.first > cur->_kv.first)cur = cur->_right;else if (kv.first < cur->_kv.first)cur = cur->_left;elsereturn false;}cur = new Node(kv);cur->_col = RED; // 新节点默认为红色if (kv.first > parent->_kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}while (parent && parent->_col == RED) // 若父节点不存在说明走到根,若父节点为黑色则不需要处理{Node *grandfather = parent->_parent; // 记录祖父节点if (grandfather->_left == parent) // 父节点在祖父节点左边时{Node *uncle = grandfather->_right; // 记录叔叔节点if (uncle && uncle->_col == RED) // 如果叔叔节点存在且为红色{// 变色parent->_col = uncle->_col = BLACK; // 将父节点与叔叔节点都变为黑色grandfather->_col = RED;            // 将祖父节点变为红色// 继续向上处理cur = grandfather;parent = cur->_parent;}else // 叔叔节点不存在或为黑色{// 需要旋转+变色if (parent->_left == cur) // cur节点在父节点左边,右单旋{RotateRight(grandfather);// 变色parent->_col = BLACK;grandfather->_col = RED;}else // cur节点在父节点右边,左右双旋{RotateLeft(parent);RotateRight(grandfather);// 变色cur->_col = BLACK;grandfather->_col = RED;}break;}}else // 父节点在祖父节点右边,和上面同理{Node *uncle = grandfather->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;parent = cur->_parent;}else{if (parent->_right == cur){RotateLeft(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{RotateRight(parent);RotateLeft(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK; // 无论如何,都将根变为黑色return true;
}


六、红黑树的验证

当我们完成了红黑树的插入函数,就已经可以构造出一棵树了

但是构造出的树是否符合红黑树的性质,则还需要我们进行验证

6.1 验证有序

在验证是否符合红黑树性质前,我们首先需要验证其是否是一棵二叉搜索树

在之前讲解二叉搜索树中提到过,如果中序遍历能够得到一个有序的序列,就说明是二叉搜索树

中序遍历代码如下:

void InOrder()
{_InOrder(_root);cout << endl;
}void _InOrder(Node *root)
{if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " "; // key/value模型,_kv是一个pair对象_InOrder(root->_right);
}

验证:

说明符合二叉搜索树性质

6.2 验证红黑树性质

通过枚举我们就能很好的控制红黑树每个节点的颜色,所以性质1可以不用额外去验证。

而性质2、3和性质4我们可以用函数来验证,代码如下:

bool IsBalance()
{if (_root == nullptr)return true;if (_root->_col == RED) //检测根是否为黑色{cout << "异常:根为红色" << endl;return false;}// 预先求出某条路径的黑色节点数量size_t blackcount = 0;Node *cur = _root;while (cur){if (cur->_col == BLACK)blackcount++;cur = cur->_left;}size_t k = 0; //作为参数传入,用于统计路径的黑色节点数量return _IsBalance(_root, k, blackcount);
}bool _IsBalance(Node *root, size_t k, size_t blackcount)
{if (root == nullptr) //走到路径结尾{if (k != blackcount){cout << "异常:路径黑节点数目不同" << endl;return false;}return true;}if (root->_col == RED && root->_parent->_col == RED) //判断是否有连续红节点{cout << "异常:出现连续红节点" << endl;return false;}if (root->_col == BLACK) //统计黑色节点数量k++;return _IsBalance(root->_left, k, blackcount) && _IsBalance(root->_right, k, blackcount); //进行递归
}

验证:


七、红黑树与AVL树的比较

红黑树和AVL树各有各的特点。前面提到,AVL树遵循的是严格平衡,也就是左右子树高度差不超过1,虽然能够很好的控制树的高度,但是在维护严格平衡时需要进行大量的旋转,对效率有不小的损耗。

而红黑树遵循的是相对平衡,通过对性质的维护时刻保持最长路径不超过最短路径的二倍,虽然高度控制的不如AVL树,但是相对的减少了旋转等操作。

而且假设我们存入100w个数,用AVL树高度大概在20层左右,换成红黑树顶多也就40层,搜索时这相差的20层对于CPU的效率来说基本可以忽略不计,所以综合而言红黑树相比AVL树是更胜一筹的。

完.

这篇关于【数据结构】如何创建一棵红黑树(附动图讲解)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

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

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

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定

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)

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

顺序表之创建,判满,插入,输出

文章目录 🍊自我介绍🍊创建一个空的顺序表,为结构体在堆区分配空间🍊插入数据🍊输出数据🍊判断顺序表是否满了,满了返回值1,否则返回0🍊main函数 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞+关注+评论+收藏(一键四连)哦~ 🍊自我介绍   Hello,大家好,我是小珑也要变强(也是小珑),我是易编程·终身成长社群的一名“创始团队·嘉宾”

《数据结构(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

Maven创建项目中的groupId, artifactId, 和 version的意思

文章目录 groupIdartifactIdversionname groupId 定义:groupId 是 Maven 项目坐标的第一个部分,它通常表示项目的组织或公司的域名反转写法。例如,如果你为公司 example.com 开发软件,groupId 可能是 com.example。作用:groupId 被用来组织和分组相关的 Maven artifacts,这样可以避免

批处理以当前时间为文件名创建文件

批处理以当前时间为文件名创建文件 批处理创建空文件 有时候,需要创建以当前时间命名的文件,手动输入当然可以,但是有更省心的方法吗? 假设我是 windows 操作系统,打开命令行。 输入以下命令试试: echo %date:~0,4%_%date:~5,2%_%date:~8,2%_%time:~0,2%_%time:~3,2%_%time:~6,2% 输出类似: 2019_06