数据结构.平衡二叉树.从二叉排序树到平衡二叉树

2024-02-07 07:32

本文主要是介绍数据结构.平衡二叉树.从二叉排序树到平衡二叉树,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

正在上数据结构的课程
感觉平衡二叉树很有对称的美感
所以决定首篇博客献给平衡二叉树

平衡二叉树其实是二叉排序树的一种提升

——那么什么是二叉排序树

  简单的讲,就是对于二叉排序树的每个节点,其左孩的值<该节点的值<右孩的值,且二叉排序树所有节点的值不重复。
如此一来,对二叉排序树进行LDR遍历的输出,便是一个递增的序列,也就是所谓的二叉“排序”树。

  由于这种有序的特点,那么查找树中的一个元素的时候,便可以产生类似“折半查找法”的效果:
1)折半查找法要求的就是一个有序的序列,二叉排序树对应的也是一个有序的序列。
2)对于要查找的元素elem

bool FindElem(BinarySortTreeNode * node,ElemType elem)
{if(node != NULL){if(node->data == elem)//找到元素return true;else if(node->data > elem)//只需继续查找左子树return FindElem(node->LeftChild, elem);else if(node->data < elem)//只需继续查找右子树return FindElem(node->RightChild, elem);}else return false;
}

所以二叉排序树又称为二叉搜索树

——为什么说是“类似”折半查找:

  这和二叉排序树的建立有关。由于二叉排序树的特点,新加入的元素newElem要符合二叉排序树的有序状态,所以插入的时候
  

bool AddElem(BinarySortTreeNode*& node, ElemType newElem)
{
|   if(node != NULL)//当node非空
|   {
|   |   if(node->data == newElem)//已存在这个元素
|   |       return false;
|   |   else if(node->data > newElem)//按规则插入node左边
|   |       return AddElem(node->LeftChild, newElem);
|   |   else if(node->data < newElem)//按规则插入node右边
|   |       return AddElem(node->RightChild, newElem);
|   }
|   else//插入到node这个位置,node是引用
|   {
|   |   node = new BinarySortTreeNode;
|   |   node->data = newElem;
|   |   //处理node其他字段
|   |   return true;    
|   }
}

(root是引用传递)

就这样利用AddElem把一棵空树不断的添加元素变成一个二叉排序树,但是这个树的形状很有可能很难看——
1)首先插入了1,那么根节点的值就是1
2)再插入2,2>1,所以2插入到了根节点的右孩
3)再插入3,3>2>1,于是插入到了根节点的右孩的右孩
……
10)再插入10,10>9>……>3>2>1,于是插入到了根节点的右孩的右孩……的右孩的右孩
结果:只有一个分支从跟到叶子。

那么再对这棵树进行查找的时候,就和普通的遍历方法无异了。
原因在于
1)折半查找法是真正的“折半”,每次比较减少了一半的元素个数
2)而二叉排序树的查找仅仅是根据节点的值与elem的比较,选择了继续查找左子树还是右子树,每次虽然不必寻找另一棵子树,但另一棵子树也许未必包含很多元素,所以并不一定达到了”折半“的效果。

——这个时候,就该平衡二叉树登场了

平衡二叉树的思想(AVL)

简单的说就是保证每个节点的左右子树差距不是很大,如此一来在查找的时候舍弃了另一棵子树也就近似于“折半”了。

于是为每个节点添加了一个字段int balance
balance = 左子树深度 - 右子树深度
(两个数相减的值可以作为这两个数的一种比较)
1)当balance的绝对值<=1的时候就认为“差距不大”,为“平衡状态”
2)当balance的绝对值>=2的时候就认为“差距过大”,为“不平衡状态”
3)定义二叉平衡树:所有节点的balance的绝对值均<=1

每次添加/删除一个元素
势必会影响到某处的深度
进而影响到balance的值
就有可能使得操作之后的二叉树不再处于平衡状态

解决办法就是:
每次添加/删除一个元素后,根据balance的变化,把不平衡的地方进行某种所谓的“旋转”使得重新变成平衡的状态。
每次的添加/删除操作都是针对一个平衡的二叉树,如果操作完成后不平衡了则重新整理成平衡状态,这就保证了二叉树时刻处于平衡状态。

平衡二叉树的旋转原理

对于一棵已经处于平衡状态的二叉树,添加/删除一个元素对其的平衡状态的影响实际上非常有限。
由于原先的balance只有三个值“0,1,-1”,所以一个元素的影响只有使得“-1变成-2”或“1变成2”才可以破坏平衡。

例如:
图1:一个平衡的二叉树
一个平衡的二叉树
图2:添加了一个元素(深色阴影)后不再平衡,可以看到其产生的影响一路向上破坏了很多节点的平衡
添加了一个元素后不再平衡
图3:应该如何旋转呢,先标记一下元素,有两个不平衡的节点A和B,我们真正旋转的部分是“浅色阴影部分”
如何旋转
图4:经过旋转(修改指针域)(乱乱哒)
旋转
图5:恢复到平衡状态(B节点只需要修改指针域,B以上的节点完全不需要改变)
恢复到平衡
——可以看到我们最需要的节点是“从新添加的节点开始向上寻找第一个不平衡的节点”,也就是图中的A,以这个节点为根节点的子树进行旋转,并改变需要A和C的balance值。
——A子树作为B的右子树,旋转后A子树的根节点变成了C,因此B的右孩也需要改变成C,所以我们还需要知道A的父节点是哪个
——但A的父节点B只有右孩需要改变,其balance值并没有改变(因为B的右子树的深度并没有改变),这也是旋转的魅力之一,使得新添加元素的影响被限制在了”浅色阴影部分“

以上的情况称为”LL型旋转“。

四种旋转情况

——旋转针对的是”以第一个不平衡的节点为根“的子树,而且这种不平衡的状态是在平衡状态的基础上由一个元素的影响产生的
——所以这样的不平衡状态情况有且仅有四种,其他的情况均不可能是在平衡状态的基础上由一个元素的影响产生的。

1)LL型旋转
——旋转前
LL旋转前
——旋转后
LL旋转后

2)LR型旋转,这里因为需要用到第三个C节点,所以有两balance值的情况
——旋转前
LR101
——旋转后
LR102
——旋转前
LR201
——旋转后
LR202
3)RR型旋转(与LL对称)
——旋转前
RR01
——旋转后
RR2
4)RL型旋转(与RR对称,同样两种balance值的情况)
——旋转前
RL101
——旋转后
RL102
——旋转前
RL201
——旋转后
RL202

对称的特点可以更好的体现在代码上,3)和4)的代码只需要把1)和2)的代码里的LefiChild和RightChild交换就可以得到。

部分代码实现

先定义树节点结构体TreeNode,要求类型T为基本类型或重载了”>”“<”“==”

template<typename T>
struct TreeNode
{T data;int balance;TreeNode *parent;TreeNode *LeftChild;TreeNode *RightChild;
};

定义一个旋转函数对旋转情况进行分类,只需要传入指向首个不平衡节点的指针即可

    void Swing(TreeNode<ElemType>*p){if (p->balance == 2){if (p->LeftChild->balance == 1)//LL型旋转SwingLL(p, p->LeftChild);else if (p->LeftChild->balance == -1)//LR型旋转SwingLR(p, p->LeftChild, p->LeftChild->RightChild);}else if (p->balance == -2){if (p->RightChild->balance == -1)//RR型旋转SwingRR(p, p->RightChild);else if (p->RightChild->balance == 1)//RL型旋转SwingRL(p, p->RightChild, p->RightChild->LeftChild);}}

LL旋转的实现

    void SwingLL(TreeNode<ElemType>*A, Tree     Node<ElemType>*B){TreeNode<ElemType> *parent = A->parent;if (parent != NULL){if (IsLeftChild(A)) parent->LeftChild = B;else parent->RightChild = B;}else root = B;A->LeftChild = B->RightChild;if (B->RightChild != NULL)B->RightChild->parent = A;B->RightChild = A;A->parent = B;B->parent = parent;A->balance = 0;B->balance = 0;}

LR旋转的实现

    void SwingLR(TreeNode<ElemType>*A, TreeNode<ElemType>*B, TreeNode<ElemType>*C){A->LeftChild = C;C->parent = A;B->RightChild = C->LeftChild;if (C->LeftChild != NULL) C->LeftChild->parent = B;C->LeftChild = B;B->parent = C;TreeNode<ElemType> *parent = A->parent;if (parent != NULL){if (IsLeftChild(A)) parent->LeftChild = C;else parent->RightChild = C;}else root = C;A->LeftChild = C->RightChild;if (C->RightChild != NULL) C->RightChild->parent = A;C->RightChild = A;A->parent = C;C->parent = parent;if (C->balance == 1){C->balance = 0;B->balance = 0;A->balance = -1;}else{C->balance = 0;B->balance = 1;A->balance = 0;}}

代码中出现的root,表示根节点,由于原本我的函数写在类内,类内又有一个root指针作为二叉树的根节点。

——END

这篇关于数据结构.平衡二叉树.从二叉排序树到平衡二叉树的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

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

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

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

hdu 1285(拓扑排序)

题意: 给各个队间的胜负关系,让排名次,名词相同按从小到大排。 解析: 拓扑排序是应用于有向无回路图(Direct Acyclic Graph,简称DAG)上的一种排序方式,对一个有向无回路图进行拓扑排序后,所有的顶点形成一个序列,对所有边(u,v),满足u 在v 的前面。该序列说明了顶点表示的事件或状态发生的整体顺序。比较经典的是在工程活动上,某些工程完成后,另一些工程才能继续,此时

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

leetcode105 从前序与中序遍历序列构造二叉树

根据一棵树的前序遍历与中序遍历构造二叉树。 注意: 你可以假设树中没有重复的元素。 例如,给出 前序遍历 preorder = [3,9,20,15,7]中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树: 3/ \9 20/ \15 7   class Solution {public TreeNode buildTree(int[] pr

【软考】希尔排序算法分析

目录 1. c代码2. 运行截图3. 运行解析 1. c代码 #include <stdio.h>#include <stdlib.h> void shellSort(int data[], int n){// 划分的数组,例如8个数则为[4, 2, 1]int *delta;int k;// i控制delta的轮次int i;// 临时变量,换值int temp;in

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图

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

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

鸡尾酒排序算法

目录 引言 一、概念 二、算法思想 三、图例解释 1.采用冒泡排序:   2.采用鸡尾酒排序:  3.对比总结 四、算法实现  1.代码实现  2.运行结果 3.代码解释   五、总结 引言 鸡尾酒排序(Cocktail Sort),也被称为双向冒泡排序,是一种改进的冒泡排序算法。它在冒泡排序的基础上进行了优化,通过双向遍历来减少排序时间。今天我们将学习如何在C