【C++】--AVL树的认识和实现

一、AVL树的概念

前面我们学习了二叉搜索树,其在一般情况下,对数据的查找的效率为O(logN),但是在极端的情况下,其时间复杂度会达到O(n)。

如下:

右边的单支或者接近单支的情况就会导致查找的效率变得很低。

所以后来又发明了AVL树,AVL树是两个前苏联科学家在1962年发表的。

其概念如下:

AVL树最先发明自平衡二叉查找树,其是一棵空树,或者具有以下性质的二叉树,它的左右子树也都是AVL树,然后左右子树的高度差不能超过1。AVL树是一棵高度平衡二叉树,其通过控制高度差去控制平衡。

如下所示:

我们此处AVL树的实现中,引用了平衡因子,其每个结点都有平衡因子,这个平衡因子是用来记录该结点的左右子树的高度差的,平衡因子的可能情况为0/1/-1。要是出现了绝对值大于1的情况,那么就说明我们当前的树不符合AVL树的要求,那么我们就要对其进行调整了,这个平衡因子不是必须要的,但是很方便我们对当前树的结构进行判断。

平衡因子=右子树高度-左子树高度

那么为啥不直接将其设计成0的平衡因子呢?

这是因为二叉树的左右子树之间高度差没办法一直保持在0的情况,比如说我们两个结点的情况,此时高度差肯定是1了。

下面是AVL树的特点:

1、高度平衡的二叉搜索树:其结点的平衡因子的绝对值保证是0或者1,避免了出现单支的情况。

2、旋转机制控制平衡:当出现平衡因子绝对值超过1的情况会通过旋转机制来保持其结构是AVL树

3、查找效率稳定:其结构稳定,不会出现极端的情况,效率为O(logn)

二、AVL树的实现

1、AVL树的结构

首先就是二叉树的标配,指向左右子结点的左右指针。然后就是要存储的数据,然后还有平衡因子,我们使用一个int类型数据,然后还有指向父结点的指针。

然后我们再设计一个头结点类,然后将这个结点结构封装到头结点中。

2、insert结点的插入

(1)、新增结点

我们整个数据插入的逻辑就是插入结点,然后更新结点的平衡因子,然后判断这个插入是否有影响整个AVL树的结构,要是影响就做调整。

首先对于空树的情况,那么我们直接插入即可。

然后数据的插入逻辑和前面学习的二叉搜索树是一样的,将插入的结点的数据从根结点开始比较,要是插入的数据大于根结点的数据,那么我们就往右边子树继续找合适的插入位置。

上面的代码,就是将我们的结点先插入到树结构中。

(2)、平衡因子的更新

插入后,我们就要更新平衡因子了,那么我们插入一个结点会对那些结点产生影响呢?

新增加结点后,其只会影响祖先结点的平衡因子,而且可能只是部分祖先结点的平衡因子会被影响,所以我们上面对于结点的设计中会多设计一个指向父节点的指针,在插入结点后对于平衡因子的更新中要经常使用到。平衡因子的更新最坏的情况就是更新到根结点的情况。然后对于啥时候更新可以停止我们下面会进行详细讲解。

平衡因子的更新原则:

平衡因子=右字数高度-左子树高度

只有子树的高度变化才会影响当前结点的平衡因子变化

然后结点的插入导致祖先结点的平衡因子要进行更新几种情况如下:

1、只会影响插入结点的父结点的平衡因子的插入:

例如这种情况,那么我们的新增结点2其是插在1这个结点是右边的,那么按照我们计算平衡因子的方式,那么我们的1结点的平衡因子就要+1。然后要是我们插入的数据是0,那么其应该插入在1这个结点的左边,那么此时1的平衡因子就-1操作。

然后对于平衡因子从0变化,这种情况下都不会破坏当前树的平衡。所以这种情况下,我们只需要将平衡因子都更新完,那么我们的插入工作就完成了。

2、会影响多个祖先结点的插入:

我们看到上面这种插入,其不止影响了其父结点1的平衡因子,然后还使得其父结点的父结点,也就是其爷爷结点的平衡因子也发生了变化。

我们发现当我们父结点的平衡因子变成0的时候,那么我们就不再需要再往上去更新再上一层父结点的平衡因子了,这是因为当其平衡因子变成0的时候,那么就说明当前子树是完全平衡的。

所以对于平衡因子的更新,当我们遇到更新为0的时候就可以停止了。

然后要是更新完是2或者-2的话,那么说明我们当前的树结构被破坏了,要使用旋转方法调整才行。

(3)、旋转调整

旋转在结点的插入中是在当AVL树的结构被破坏的时候使用的。其目的是保持当前树的结构是AVL树,就树的左右高度差保持在1以内。子树之间也是如此。

旋转主要分为四种:左单旋/右单旋/左右双旋/右左双旋。

下面我们对这几种旋转进行详细讲解:

1、右单旋

左单旋,其是应对单单左边子树不平衡的情况,那么树高的部分就要将其和右边子树进行调整

那么其是如何进行调整的呢?

下面我们看几个示例图:

首先我们来看看最简单的,就是h此时为0或者1的时候:

上面是h为0的时候,那么我们发现,经过我们上面的调整后,我们的树的结构还是保持着AVL树的结构。

下面我们看看h为1的情况:

下面是h为3的情况:

我们会发现随着高度的变化,我们新插入的结点的情况就越多,变化特别大。

所以我们将其总结成下面这种情况:

如图所示,我们的a子树,其高度为h,此时a处插入一个结点,导致a子树的高度变为h+1,然后平衡因子更新,导致根结点的平衡因子更新为-2,那么此时就需要进行旋转调整树的结构了。

那么我们看上图,其调整逻辑是将a的父结点的父亲结点变成a的父结点的右结点,然后原来a的父结点的右子树变成原来a的父结点的右孩子的左孩子。要注意的是b可能是空的,然后要是parent是根结点的情况,那么我们也要特殊处理胰一下,避免空指针的访问。

下面是右旋的代码:

这一部分我们通过代码,然后将上面的图按照这一段代码的逻辑进行移动,我们就懂了右旋的逻辑。

然后我们啥情况下使用右单旋呢?

我们观察图中可以看出,当cur的平衡因子是-1的时候,然后cur->parent的平衡因子是-2的时候就要使用我们的右单旋。

2、左单旋

左单旋的话和我们的右单旋的情况有点类似。

其情况如下:

插入一个新结点后,会导致右边不平衡,然后我们要将其向左边进行调整

然后我们的调整方法如下:

如上所示,我们将sudR的左子树变成其父结点的右孩子,然后其父结点变成其左孩子。

然后要注意的是,我们的sudRL可能是空的,所以要加一个条件判断。

然后使用其的情况是,当sudR的平衡因子为1,parent的平衡因子是2的时候,那么此时就使用我们的左单旋。

代码如下:

3、左右双旋

这种情况的话,就是插入到我们的b的位置,然后导致我们的树是中间部分的树更高。

那么我们就没办法调用左右单旋了。

如上,要是新插入一个结点后,那么此时是中间的高,然后我们再看左边子树,这个子树内部是右边的要高。

然后这种情况又一共分为三种场景:

下面我们对上面三种情进行一步步的拆解看看其是如何进行旋转的:

首先是场景1:

对于上面这个树,对于parent这棵树,是左边要高,但是对于其左子树sudL这个结点,其是右边子树要更高,所以我们对于sudL这个子树要使用的是左单旋,然后对于parent这个结点要使用的是右旋。

所以其旋转步骤变化图如下:

首先是对于sudL这棵子树的左旋。然后使得整个树的结构变成如下的情况:

我们看到我们此时的树对于parent这个结点来说就是左边高右边低的了,然后其内部的子树也是如此,那么其就回到了我们前面的单旋的时候的结构了,那么我们再对parent这个结点使用一次右单旋转即可。

旋转完成如下:

然后我们的场景二也是类似:

场景三:

然后对于三种场景的平衡因子的更新都有所不同:

在场景一的情况下,首先新增的结点插入在e这个位置,那么导致其祖先结点8->5->10的平衡因子不断更新,到10这个结点的平衡因子的绝对值超过了1,导致旋转。其中sudLR这个结点的平衡因子在旋转前为-1,然后旋转后sudLR和sudL的平衡因子都为0,parent的平衡因子变为1。

在场景二的情况下,新增的结点是插入在f这个子树的,然后旋转前sudLR的平衡因子为1,旋转后sudRL和parent的平衡因子都变为0,sudL的平衡因子为-1。

场景三的情况下a/b/c子树都是空树,然后是b自己进行新增一个结点,然后对sudL和parent的平衡因子进行更新,旋转前sudLR的平衡因子为0,旋转后sudL、sudLR、parent的平衡因子均为0。

然后其代码实现我们会进行前面的左单旋和右单旋进行复用。

其触发的条件为parent和sudL的平衡因子在插入结点后,parent更新为-2,然后sudL的平衡因子更新为-1。

代码如下:

4、右左双旋

右左双旋和我们上面的左右双旋转的逻辑大差不差。

其使用的情况如下:

其是插入到右子树的左边子树。然后我们右边子树内是左边的子树要更高一点,所以我们要先调整这个子树,使用右单旋,然后再对整个树使用左单旋。

然后就是平衡因子的更新了。

我们的右左单旋也是有三种场景:

场景一:

场景二:

场景三:

然后对于这三种的平衡因子的更新也是不一样的:

对于场景一,其新增结点是插入在e树的,其插入影响了sudRL、sudR、parent的平衡因子,在旋转前,sudRL的平衡因子为-1,然后旋转后,parent和sudRL的平衡因子都变成了0,sudR的平衡因子为1。

然后对于场景二,新增结点是插入在f树的,其影响了sudRL、sudR、parent的平衡因子,在旋转前sudRL的平衡因子为1,旋转后,sudRL和sudR的平衡因子都变成0,然后parent的平衡因子变成了-1。

对于场景三,a/b/c都是空树,所以b是自己新增的一个结点,然后导致sudR和parent的平衡因子变化,旋转前sudRL的平衡因子为0,旋转后,sudRL、sudR、parent的平衡因子都变成了0。

代码如下:

然后我们注意要使用右左单旋的情况:

当parent的平衡因子为2,然后parent->_right的平衡因子为-1的时候使用。

如下:

3、AVL树的查找find

AVL树的查找逻辑和我们的二叉搜索树是一样的:

不过要注意的是我们的查找逻辑是使用关键码key进行查找。

4、求树高

这个也很简单,和前面学习二叉树的时候求二叉树的树高是一样的。

5、求AVL树的结点个数

这个就直接进行一个递归调用就可以:

6、AVL树的平衡检测

这个检测的核心目标是防止一个结点的左右树高超过了1,那么这棵树就违反了AVL树的要求,那么我们是否可以使用平衡因子进行检测呢?

其实不行,我们要是使用平衡因子进行判断,那么我们不就即成了规则的制订者,又成了裁判了。

我们的判断代码如下:

通过求各个子树之间的高度差进行判断。

三、AVL树实现完整代码

复制代码
#pragma once
#include<iostream>
#include<assert.h>

using namespace std;

template<class K ,class V>
struct AVL_TreeNdoe
{
	AVL_TreeNdoe(const pair<K,V>&kv)
		:_kv(kv)
		,_parent(nullptr)
		,_left(nullptr)
		,_right(nullptr)
		,_bf(0)
	{

	}

private:
	pair<K, V>_kv;
	AVL_TreeNdoe<K, V>* _parent;
	AVL_TreeNdoe<K, V>* _left;
	AVL_TreeNdoe<K, V>* _right;
	int _bf;
};

template<class K,class V>
class AVL_Tree 
{

	typedef AVL_TreeNdoe<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		//要是树为空,直接插入
		if (_root==nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		
		//创建一个结点记录插入位置的父结点,方便插入结点和树进行链接
		Node* parent = nullptr;
		//先找到新插入结点合适的插入位置,然后进行插入操作
		Node*cur = _root;
		while (cur)
		{
			parent = cur;
			//注意比较的关键码是key
			//插入的数据大于根结点,往右边插
			if (cur->_kv.first<kv.first)
			{
				cur = cur->_right;
			}
			//插入的数据小于根结点,往左子树插
			else if(cur->_kv.first>kv.first)
			{
				cur = cur->_left;
			}
			//插入的数据树中已存在,插入失败
			else
			{
				return false;
			}
		}
		//此时cur所在的位置就是结点插入的合适的位置
		cur = new Node(kv);
		cur->_bf = 0;

		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		//插入的结点和树进行链接
		cur->_parent = parent;

		//更新平衡因子_bf
		while (parent) 
		{
			if (parent->_left=cur)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}


			if (parent->_bf==0)
			{
				return true;
			}
			else  if(parent->_bf==1||parent->_bf==-1)
			{
				//继续更新祖先结点的平衡因子
				cur = parent;
				parent = parent->_parent;
			}
			else if(parent->_bf==2||parent->_bf==-2)
			{
				//旋转调整
				if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if(parent->_bf==2&&cur->_bf==1)
				{
					RotateL(parent);
				}
				else if(parent->_bf==-2&&cur->_bf=-1)
				{
					RotateLR(parent);
				}
				else if(parent->_bf==2&&cur->_bf==-1)
				{
					RotateRL(parent);
				}
			}
			else
			{
				//程序前面就有问题了
				assert(false);
			}
		}
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

	bool _IsBalanceTree(Node* root)
	{
		if (root == nullptr)
			return true;

		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		int bf = rightHeight - leftHeight;
		if (abs(bf) >= 2 || bf != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		return _IsBalanceTree(root->_left)
			&& _IsBalanceTree(root->_right);
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	int _Size(Node* root)
	{
		return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
	}

private:
	void RotateR(Node* parent)
	{
		Node* subl = parent->_left;
		Node* sudlR = subl->_right;

		parent->_left = sudlR;

		if (sudlR)
		{
			sudlR->_parent = parent;
		}

		Node* parentparent = parent->_parent;

		sudl->_right = parent;

		parent->_parent = sudl;

		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (parentparent->_left == parent)
			{
				parentparent->_left = subL;
			}
			else
			{
				parentparent->_right = subL;
			}

			subL->_parent = parentparent;
		}

		parent->_bf = subL->_bf = 0;
	}
	
	void RotateL(Node* parent)
	{
		Node* sudR = parent->_right;
		Node* sudRL = sudR->_left;

		parent->_right = sudRL;

		if (sudRL)
		{
			sudRL->_parent = parent;
	    }

		Node* parentparent = parent->_parent;

		sudR->_left = parent;
		parent->_parent = sudR;

		if (parent==_root)
		{
			_root = sudR;
			sudR->_parent = nullptr;
		}
		else
		{
			if (parentparent->_right==parent)
			{
				parentparent->_right = sudR;
			}
			else
			{
				parentparent->_left = sudR;
			}
			sudR->_parent = parentparent;
		}
		sudR->_bf = 0;
	}

	void RotateLR(Node* parent)
	{
		Node* sudL = parent->_left;
		Node* sudLR = parent->_left->_right;
		
		//将旋转前sudLR的平衡因子记录下来,方便后续对旋转后的平衡因子进行更新
		int bf = sudLR->_bf;

		//先对左子树进行左单旋
		RotateL(parent->_left);
		//再对整体进行右单旋
		RotateR(parent);

		//旋转完成对平衡因子进行更新
		if (bf==0)
		{
			parent->_bf = 0;
			sudL->_bf = 0;
			sudLR->_bf = 0;
		}
		else if(bf==-1)
		{
			sudL->_bf = 0;
			sudLR->_bf = 0;
			parent->_bf = 1;
		}
		else if(bf==1)
		{
			sudL->_bf = -1;
			sudLR->_bf = 0;
			parent->_bf=0
		}
		else
		{
			assert(false);
		}
	}

	void RotateRL(Node* parent) 
	{
		Node* sudR = parent->_right;
		Node* sudRL = sudR->_left;

		int bf = sudRL->_bf;
		
		//先对右边子树进行右旋
		RotateR(parent->_right);
		//再对整体进行左旋
		RotateL(parent);

		//旋转完成,对平衡因子进行更新
		if (bf==0)
		{
			parent->_bf = 0;
			sudR->_bf = 0;
			sudRL->bf = 0;
		}
		else if (bf==1)
		{
			sudR->_bf = 0;
			sudRL->_bf = 0;
			parent->_bf = -1;
		}
		else if(bf==-1)
		{
			sudR->_bf = 1;
			sudRL->_bf = 0;;
			parent->_bf = 0;
		}
	}

private:

	Node* _root;
};
相关推荐
程序员zgh2 小时前
常用通信协议介绍(CAN、RS232、RS485、IIC、SPI、TCP/IP)
c语言·网络·c++
栀秋6662 小时前
“无重复字符的最长子串”:从O(n²)哈希优化到滑动窗口封神,再到DP降维打击!
前端·javascript·算法
xhxxx2 小时前
不用 Set,只用两个布尔值:如何用标志位将矩阵置零的空间复杂度压到 O(1)
javascript·算法·面试
有意义2 小时前
斐波那契数列:从递归到优化的完整指南
javascript·算法·面试
aningxiaoxixi2 小时前
TTS 之 PYTHON库 pyttsx3
开发语言·python·语音识别
暗然而日章2 小时前
C++基础:Stanford CS106L学习笔记 8 继承
c++·笔记·学习
有点。3 小时前
C++ ⼀级 2023 年06 ⽉
开发语言·c++
Mr.Jessy3 小时前
JavaScript高级:深入对象与内置构造函数
开发语言·前端·javascript·ecmascript
charlie1145141913 小时前
编写INI Parser 测试完整指南 - 从零开始
开发语言·c++·笔记·学习·算法·单元测试·测试