首页 > 编程语言 >【C++】AVL树

【C++】AVL树

时间:2024-12-13 14:04:01浏览次数:8  
标签:10 结点 parent C++ AVL 因子 平衡

AVL树

概念

  • AVL树是最先发明的⾃平衡⼆叉查找树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的左右⼦树都是AVL树,且左右⼦树的⾼度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树, 通过控制⾼度差去控制平衡

  • AVL树得名于它的发明者G.M.Adelson-Velsky和E.M.Landis是两个前苏联的科学家,他们在1962 年的论⽂《Analgorithmfortheorganizationofinformation》中发表了它。

  • AVL树实现这⾥我们引⼊⼀个平衡因⼦(balancefactor)的概念,每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1, AVL树并不是必须要平衡因⼦,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡, 就像⼀个⻛向标⼀样。

  • 为什么AVL树是⾼度平衡搜索⼆叉树,要求⾼度差不超过1,⽽不是⾼度差是0呢?不是不想这样设计,⽽是有些情况是做不到⾼度差是0的。⽐ 如⼀棵树是2个结点,4个结点等情况下,⾼度差最好就是1,⽆法作为⾼度差是0

  • AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 l o g N logN logN ,那么增删查改的效率也可以控制在 O ( l o g N ) O(logN) O(logN),相⽐⼆叉搜索树有了本质的提升

image-20241208212507207

这个就不平衡了:

image-20241208213129066

AVL树的实现

结点结构:

以前我们的树结点指针里存放值和左右孩子结点指针,现在多出了一个parent结点以及平衡因子,而且存的值是pair类型的。

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	
	AVLNodeTree<K,V>* _left;
	AVLNodeTree<K,V>* right;
	AVLNodeTree<K,V>* parent;

	int _bf;

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,bf(0)
	{}
	
};

树的大概框架:

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
//……
private:
	Node* _root = nullptr;
};

AVL树的插入

我们可以先写出和原本的区别不大的部分,比如肯定都是先比当前值小就往左走,比当前值大就往右走……

bool insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
		_root = new Node(kv);
	return true;
	
	Node* cur = _root;
	Node* parent = _root;
	while (cur)
	{
		if (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (kv > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return false;
		}
	}
	
	cur = new Node(kv);
	cur->_parent = parent;
	//……
	return true;
}

我们先停在这一步,接下来我们需要考虑怎么去更改原本存在的结点的平衡因子以及控制平衡的问题,因为新插入的结点的平衡因子肯定是0不需要改,直接用缺省值。

image-20241208214352161

我们现在只做完了第一步。

更新方法预览:

image-20241208215551042

注意更新原则的第2点,只有子树高度变化才会影响当前平衡因子。也就是说对于插入结点的某个祖先来说,如果其实左右子树高度差并没有变化,就没有影响到它的平衡因子,也就是插入一个结点并不一定会影响它的全部祖先。

比如:

image-20241208215320368

对于最上面这个节点来说,原本平衡因子就为-1,插入后还是-1

下图我们可以看到更新平衡因子我们需要倒着往祖先走不断更新直到不需要再更新,所以这就是为什么要在结点里设计parent指针。

有些地方有人没有设计parent。而是通过栈或vector将路径存起来:8 10 14 13。更新的时候就在容器中不断去取。但有parent更便利。parent不是必须的。

image-20241208215538551

更新原则的第3点好理解。

第4点:我们从第3点原则知道插入结点的parent结点的平衡因子是肯定会改变的,要么++要么–,但是是否会继续往上更新,得看parent所在子树的高度是否变化

如果本来这棵树就没有变高,parent以上的结点的平衡因子就不会改变。

所以接下来的关键问题就是,怎么知道parent所在子树的高度是否变化呢

仍然以上面这张图为例,我们看更新停止条件的第2点:

更新后parent的平衡因⼦等于1或-1,parent所在子树高度肯定变了。因为这说明parent的平衡因子的变化过程要么是0->1,要么是0->-1,因为根据我们的更新原则的第3点,这个parent的平衡因子要么是++得到的要么是–得到的,说明它原来是0.(倒推思考)因为如果不是这样的话,插入之前根本就不是AVL树了。(我们前提它是一颗AVL树,要插入后继续保持为AVL树);所以这种情况的高度就增加了,所以**要继续往上更新平衡因子**。

怎么更新呢?

image-20241208231029678

如上图,parent往上走,cur也往上走

可以看到对于现在的parent来说,插入结点是在右子树的,所以平衡因子要++,1->2。

所以现在我们来到了更新停止条件的第3种:更新后parent的平衡因子等于2或-2:同样我们进行倒推,++或者–来到2或者-2,说明原本是1->2或者-1->-2,同样只能是这两种情况,否则原本根本就不是AVL树了。这种情况说明我们的插入结点到了本来就更高的那一边,破坏了平衡,可以说是“雪上加霜”。我们需要旋转处理。旋转之后也不需要再往上继续更新了,因为旋转顶多让它的高度降低1,而原本是AVL树,降低1又变回了AVL树。

我们再来看下面这张图,parent走到3,因为插入结点在其左子树,平衡因子–,也就是**更新后parent的平衡因子等于0的情况:倒推得出是从1->0或者从-1->0的。这说明原本这棵树是一边高一边低,但是现在变成平衡了,所以也不用继续往上更新了**。这种让不平衡的树变平衡了的,可以说“雪中送炭”。

image-20241208232219043

所以如果更新后parent的平衡因子为0,我们才不需要继续更新,否则就要更新。

可以说更新的继续条件是更新后parent的平衡因子为1或-1,而更新停止的条件是更新后parent的平衡因子为0或2或-2.

但在一种情况下,更新后parent的平衡因子为1或-1,却也结束了

image-20241208234248398

也就是已经更新到根结点的情况。如果parent都已经走到根了,也没法再继续往上更新了。

所以现在我们可以写出平衡因子更新的代码了:

while (parent)
{
	if (parent->_left == cur)
		parent->_bf -= 1;
	else
		parent->_bf += 1;
	if(parent->_bf)

	if (parent->_bf==0)
	{
		break;
	}
	else if (parent->_bf == 1 || parent->_bf == -1)
	{
		cur = parent;
		parent = parent->_parent
	}
	else if (parent->_bf == 2 || parent->_bf == -2)
	{
		//旋转
	}
	else//这是一定不能出现的情况,直接断死
	{
		assert(false);
	}
}

旋转

旋转的原则

1.保持搜索树的规则

2.让旋转的树从不平衡变平衡,其次降低旋转树的高度

旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋

右单旋

image-20241209193929941

本图1展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要 求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。这⾥a/b/c是⾼度为h的⼦树, 是⼀种概括抽象表⽰,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体图2/图3/图4/ 图5进⾏了详细描述。

在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平 衡因⼦从-1变成-2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树左边太⾼了,需要 往右边旋转,控制两棵树的平衡。

旋转核⼼步骤,因为5<b⼦树的值<10,将b变成10的左⼦树,10变成5的右⼦树,5变成这棵树新
的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原
则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

具体来看:

image-20241209204009323

情况1:

h为0

假如abc现在是这种情况,也就是都为空树,插入a后向上调整到10发现平衡因子为-2,所以需要旋转。

怎么旋转?让5的右子树也就是b变成10的左边,10变成5的右子树,就得到了最右边的这种。

相当于把10摁下去了。

这就是抽象的情况之外的具体情况1,是较为简单的一种情况:插入前abc高度都为h而h为0.

情况2:

h为1

image-20241209205208431

这就已经跟抽象的那张图长得十分相似了

可以看到就是把5的右子树变为10的左子树,然后把10变为5的右子树

所以从上面我们可以看出,无论h是0还是1,旋转的逻辑都是一样的。

情况3:

h=2

这就比较复杂了。image-20241209211740778

image-20241209211722101

如解释,36种情况。

情况4:

h=3

这个又复杂得多了

image-20241209213200769

第三层可以有1个结点,2个,3个,各自有4 、6 、4种情况,一共14种,再加上满二叉树情况,一共15种情况。

bc各有15种情况,所以bc有15*15=256种情况。

我们再来看a的情况:a要保证的是插入了之后自己没有发生旋转(也就是在a子树以上的部分进行旋转),但是高度又变了所以往上调整。

a如果是x这种满二叉树的结构,插入位置有8个。

其次,如果a不是满二叉树的结构,a的结构最后一层必须保留3个结点,否则也无法满足我们要在a插入后自己没有发生旋转但高度又改变的条件。

比如:假设只有1个结点:

image-20241209214548531

如果插入到左下角结点,自身就要旋转了;如果插入到右边3个位置则y-C这棵树的高度并没有变化,也不会引发以上的结点的平衡因子的变化。

image-20241209215138431

我们可以对比一下这两种情况,左边这种是4个结点的左右孩子处都可以插入,也就是8种情况;右边这种是只能在有两个结点的位置插入,因为如果在左边要么就不会引发向上更新要么就自身旋转了。4个结点保留3个有4种情况,每种情况都在有两个结点的那边插入,有4种插入情况(因为有两个节点),所以一共有8+4*4种a的情况。

a的情况*bc的情况就是15 *15(8+4 *4)最终为5400种情况。

这个a的推论就是最复杂的。

不过不论是哪种情况,旋转逻辑都是一样的。

左单旋

是右边更高

image-20241209171956222

图1:

这是一个10为根的树,有a、b、c抽象为三棵高度为h(h>0)的子树,a、b、c均符合AVL树的要求。

我们可以看到,15这个结点的平衡因子是0,10这个节点的平衡因子是1.

10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。

这是一种概括抽象表示,这代表了所有右单旋的场景,实际右单旋形态有很多种。

图2:

可以看到在a子树中插入一个新节点,导致a子树的高度从h变成h+1,不断向上更新平衡因子,导致10的平衡因子从1变成2,10为根的树左右高度差超过1,违反平衡规则。所以需要往左边旋转,控制两棵树的平衡。

旋转方式:b变成10的右子树,10变成15的左子树,15成为这棵树的新的根。因为b一定比10大,所以可以成为10的右子树;10比15小,所以可以变成15的左子树。这都满足二叉搜索树规则。

image-20241209192506927

可以看到我们得到的树没有问题。

10、b、c都比15小,所以做它的左子树没有问题。

现在对于15来说,左右子树的高度就一样了。

可以想象成把10这个位置往下摁。

本文结束,下一篇文章讲解具体怎么来写旋转的代码。

标签:10,结点,parent,C++,AVL,因子,平衡
From: https://blog.csdn.net/2301_82135086/article/details/144371424

相关文章

  • C++中多态性在实际项目中的应用场景有哪些?
    一、图形绘制系统:在一个图形绘制系统中,可以定义一个抽象的图形类,然后派生出各种具体的图形类,如圆形、矩形、三角形等。通过多态性,可以使用一个统一的接口来绘制不同类型的图形,而不需要为每种图形都编写单独的绘制函数。二、游戏开发在游戏开发中,不同的游戏角色可能有不同的......
  • 关于数据隐藏:为什么要进行数据隐藏?如何在C++中实现数据隐藏?以及数据隐藏对面向对象编
    一、为什么要进行数据隐藏?数据隐藏可以提高程序的安全性和可维护性。可以将数据成员声明为私有或受保护,可以防止外部代码直接访问和修改这些数据,从而减少错误的发生。同时,数据隐藏也使得类的内部实现细节对外部不可见,这样在修改类的内部实现时,不会影响外部代码的使用。二、......
  • C++ STL常用容器之deque&list
    文章目录一、序列式容器二、双端队列deque2.1容器属性2.2Deque特点三、迭代器操作3.1使用迭代器完成3.2迭代器函数四、双向链表list4.1容器属性4.2list特点4.3相比vector新增函数五、vectordequelist之间的区别六、vector&deque&list之间的转换一、序列......
  • C++实现希尔排序算法
    指定格式输入字母(字母间以空格分隔),按照希尔排序输出指定格式#include<iostream>#include<vector>#include<string>usingnamespacestd;voidshellSort(vector<string>&arr){ intn=arr.size(); //初始步长设置为数组长度的一半,后面逐步缩小步长直到值为1为止 for......
  • 面试必会(嵌入式)-C++面试高频(一)
    目录1.new和malloc的区别(使用和原理)⭐new的定义:malloc的定义:new与malloc的区别:(简单理解)new与malloc使用区别2.struct和class的区别⭐3.char和int之间的转换4.什么是野指针和悬挂指针⭐5.NULL和nullptr区别⭐6.指针常量和常量指针有何区别⭐7.虚拟内存和物理内存的......
  • C++构造函数和析构函数
    目录1构造函数1.1什么是构造函数?1.2无参构造函数1.3带参数构造函数2析构函数2.1什么是析构函数?1构造函数1.1什么是构造函数?类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造,那构造的是什么呢?构造成员变量的初始化值,内存空间等构造......
  • 12C++循环结构-for循环(2)——教学
    一、循环变量为字符型试编一程序,按字典顺序输出26个字母。流程图:思考:先顺序输出26个小写英文字母,再逆序输出26个大写英文字母。循环可以是递增型循环,也可以是递减型循环。二、打擂台-for语句的另一种形式问题:试编一程序,输入10个数,输出其中最大的数。以前学过,输入三个数求......
  • C++ debug
    C++debug在C++中,查看程序的调用栈(CallStack)通常用于调试崩溃、性能问题或逻辑错误等场景。以下是几种常用的方法来查看调用栈:1.使用GDB调试器查看调用栈GDB(GNUDebugger)是Linux上非常流行的调试工具,可以用来查看C++程序的调用栈。示例:假设有以下C++程序:#includ......
  • [C++] 继承详解
    目录前言演示用编译器及其标准DevC++6.7.5Redpanda C++14                           先 赞 后 看  养  成 习 惯  正文1、继承的概念与意义2、继承的使用 2.1继承的定义及语法2......
  • C++_运算符重载
    filesystemc++11在CMakeList.txtfind_package(BoostCOMPONENTSsystemfilesystemregexREQUIRED)include_directories(${Boost_INCLUDE_DIRS})target_link_libraries(projectname${Boost_LIBRARIES})程序#include<boost/filesystem.hpp>......