数据结构之二叉查找树(Binary Search Tree)和红黑树(Red Black Tree)

简介: 二叉查找树又可以称之为 : 二叉搜索树 , 二叉排序树 , 它或者是一棵空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势 , 下图中这棵树,就是一棵典型的二叉查找树

二叉查找树(Binary Search Tree)

二叉查找树又可以称之为 : 二叉搜索树 , 二叉排序树 , 它或者是一棵空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根节点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势 , 下图中这棵树,就是一棵典型的二叉查找树


a0c40e12c2002365f67b8a33f0cccdd1_f94dc3fdb6ca098db8b991dbc1f36a0b.png


那么这样的数据结构有什么好处呢?

比如我们来查找一个元素 : 23


如果我们用链表来存储的话 : 查找元素23就需要一个一个遍历 , 直到找到匹配的元素


f6979475a37b5a73da2bca0ee4cbc5a7_f9b7e1ac0a2c0871398238d2426385a2.png


如果使用二叉查找树来存储的话 , 查找元素23 , 首先和根节点20比较 , 发现比20大 , 那么就查找树右边的元素 , 发现比30小 , 那么就继续查找30左边的元素 , 发现比25小 , 那么就查找25左边的元素 , 这样一来 , 整个元素查找所消耗的时间远远的比遍历整个链表快了许多 , 这就是二叉查找树的思想 , 查找所需的最大次数等于这个树的高度


在插入结点的时候也是使用类似的方法 , 找到元素的位置然后插入 , 虽然看起来挺方便的 , 但是也有它的缺陷 , 比如这种情况 : 原有的树结构为这样(如下图所示) , 现在需要插入9,8,7,6,5这几个元素


fe403f57b6fd42cf9f221763235f1f1a_ab61fd03426c370ea782e4033e48c767.png


元素插入之后就变成这样了(如下图所示)


2c8b3eef420a9653b8501c7d3302eeb8_d1a5691e6a28b507160699af7b000eb0.png


可以发现 , 这样的存储和链表没什么太大的区别 , 虽然符合二叉查找树的特性 ,但是查找的性能大打折扣


那么如何解决二叉查找树多次插入新结点导致的不平衡呢?

这个时候红黑树诞生了 , 红黑树(Red Black Tree) 是一种自平衡的二叉查找树 , 它除了符合二叉查找树的基本特征外还包括了以下特征 :


根节点必须是黑色的


节点是红色和黑色


每个叶子节点都是黑节点的空节点(NIL节点)


每个红色节点的两个子节点都是黑色的(从每个叶子到根的所有路径上不能有连续的两个红色节点)


从任意节点到每个叶子的所有路径上的黑色节点数量相同


我们用下面的一张图来表示 :


c6495e3688b43f9d88f901a3a1d929b2_b32e706341e2502edbaaae5549adbfa4.png


这个图看起来比二叉查找树的图复杂一点 , 正是有了这些规则的限制 , 才保证了红黑树的自平衡 , 红黑树从根到叶子的最长路径不会超过最短路径的2倍


当进行插入和删除节点操作的时候 , 这个树的平衡可能被打破 , 所以就需要一些措施来保证这个树的平衡 , 那么在什么情况下会打破这棵树的平衡呢? 下面来看几种例子


情况一 : 插入一个元素23

插入一个元素28之后 , 由于父亲节点是黑色节点 , 并不会打破红黑树的规则 , 所以没有问题


54c3ccdfa7e42963ceb939cac7c2f3c7_c4a8d0593bd4350bf5aef59bff995eb3.png


情况二 : 插入一个元素33


dd540b65edf2480708b6840e5ddb841c_cab12253e0d135dc806e8ab4df6d378a.png

为什么插入的这个节点是红色的?

因为上面提到红黑树的第五个特征就是 : 从任意节点到每个叶子的所有路径上的黑色节点数量相同 , 所以这块不能是黑节点 , 只能是红节点


由于父亲节点是红色节点 , 因此打破了红黑树的规则 : 每个红色节点的两个子节点都是黑色的(从每个叶子到根的所有路径上不能有连续的两个红色节点) , 所以就必须做出调整 , 使它符合红黑树的规则


那么应该做出什么样的调整呢?

旋转(左旋右旋)变色


旋转和颜色变换规则:所有插入的点默认为红色

变色

变色的情况 : 当前节点的父节点是红色的 , 当前节点的叔叔节点是红色的


下面我们实际分析一下:


dd540b65edf2480708b6840e5ddb841c_cab12253e0d135dc806e8ab4df6d378a.png


现在我们插入一个节点33 , 由于新插入的节点是红色的 , 打破了红黑树的平衡 , 所以要进行旋转变色来维持这个平衡


首先就是判断是旋转还是变色: 33的父节点是红色 , 叔叔节点也是红色 , 符合变色规则


先把父亲节点35变为黑色 , 再把叔叔节点45变为黑色 , 把爷爷节点40变为红色



435a9dffde49fbefdb82c305c4bfc2b2_a1bcd3d5390c5c9baffbf5c2bf787d46.png

由于40的父亲节点30和叔叔节点10也都是红色 , 所以要继续改变颜色 , 把父亲节点30变为黑色 , 把叔叔节点10变为黑色 , 爷爷节点20变为红色


459b2f1e06dcd3218e746fa788c434c3_c9cd20b11f4ccdafcd1502bf283fab45.png


由于爷爷节点是根节点 , 根节点只能为黑色 , 所以变色为黑色 , 这样 , 我们得到了一棵新的红黑树


cbcaefd80f224d1680bfff80591cdeef_f51239263529ce394df0614ef743f64d.png


左旋

当前父结点是红色,叔叔是黑色的时候,且当前的结点是右子树 , 以父节点为中心逆时针旋转,使得父节点被自己的右孩子取代,而自己成为自己的左孩子


b630fb6a1f32e425e051765c989dc389_6a7265c1eabd093874e9437a5e19c77c.png


插入一个元素41


021f09962ee658acfe9f6c30e4d23db8_9eaecd88ea844ec98dcaeb37f7940aa5.png


这个时候红黑树的规则又被破坏了 , 继续进行判断 , 发现符合变色的条件 , 进行变色


91252f76479387574a539e6eeda15aa6_7c905618b88c1e414183b17be04b7609.png


变色完成之后 , 发现40和35又破坏了红黑树的规则 , 继续进行判断 , 40的父节点是红色 , 叔叔节点是黑色的 , 不符合变色的条件了 , 但是符合左旋转的条件 , 所以以父节点为中心逆时针旋转进行左旋


5bb3d5c1b1844f0675c27459b9ee66b2_a6bde9c941fd2726d44a0552dc91cb84.jpeg


左旋之后 , 发现35和40节点还是不符合红黑树的规则 , 也不符合变色和左旋的规则 , 所以我们接着这个结果在下面分析右旋


右旋

当前节点的父节点是红色的 , 叔叔节点是黑色的 , 且当前节点是左子树 , 以自己的爷爷节点为中心顺时针旋转 , 使得父节点被自己的左孩子取代,而自己成为自己的右孩子


接着上面左旋的结果来分析 , 由于符合右旋的结果 , 所以我们围绕父节点40来进行顺时针旋转 , 旋转后结果如下


48461840161c7107467ea310dad29296_8eb52fc73a7ec1b2556ff547dbc42a05.png


右旋之后40节点成为了根节点 , 由于根节点只能是黑色 , 所以进行变色


41125b2948d36a95832810cf17fafe17_81f28cd4034e9a96a92172d40cf6a5f2.png


变色之后发现还是不满足红黑树的规则4 , 所以把45节点再进行变色


656e68068dba31c1f95250186c47a80d_8f0ee42bf107a389ad71c63b43387963.png


插入新节点41之后 , 经过一系列的旋转变色操作 , 最终得到了一棵红黑树


以上我们通过图文的方式简单的分析了红黑树的旋转变色 , 在实际的插入删除中 , 还涉及到很多的条件 , 这里就不一一列举了 , 主要体会红黑树自平衡调整的主体思想

相关文章
|
2天前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
31 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
2天前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
26 12
|
2天前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
27 10
|
2天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
15 2
|
2月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
261 9
|
2月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
42 1
|
2天前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
112 75
|
2天前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
24 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
2天前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
25 9
|
2天前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
21 7