【数据结构取经之路】图解AVL树

目录

AVL树的前世今生

走进AVL

AVL树的实现

1、AVL树节点的定义

2、AVL树的插入

3、完整代码

AVL树的性能


AVL树的前世今生

我们知道,普通的二叉搜索树在不少场景下可以提高我们的查找效率。例如下面这种情况,我们要找22。从根开始,一比较就直接排除了将近一半的数据,实在是快活呀!

但是先别高兴的太早,如果不出意外的话,意外就要发生了。在数据有序或者接近有序的场景下,它就招架不住了。请看:

这时二叉搜索树已经退化为了单支树,查找的效率就相当于在顺序表中搜索元素了。结构比顺序表复杂,而且效率还和顺序表一样,你说,我要你有何用?!所以,二叉搜索树呀,虽然你在不少场景下表现的很好,但是还不够! 这时普通的二叉搜索树急需改进。按照剧情发展,每当到这种关键时刻,一定会有能人站出来"挽狂澜于既倒,扶大厦之将倾"。没错,俄罗斯的两位数学家站了出来。

走进AVL

书接上回。1962年,俄罗斯的两位数学家G.M. Adelson-Velsky和E.M. Landis提出了一种解决普通二叉搜索树退化的方案:

当向二叉搜索树中插入节点后,如果能保证每个节点的左右子树高度之差的绝对值不超过1,那么就可以降低树的高度,从而减少平均搜索长度。

有人也许会有这样的疑问:为什么不保证每个节点的左右子树高度之差为0呢?我猜当年俄罗斯的那两位数学家也想过这个问题,他们既然考虑到了这一点,为何这两位大佬不把AVL树设计成左右子树高度差为0呢?是他们水平不行?!那还真不是。你想想,如果这棵树只有2个节点、4个节点,那么根本无法做到绝对平衡。

一棵AVL树,要么是空树,要么是具有以下性质的二叉搜索树:

● 它的左右子树都是AVL树。

● 左右子树的高度之差(平衡因子)的绝对值不超过1。

上面引出了一个叫平衡因子的概念。那么,我的问题来了,什么是平衡因子?为什么要有平衡因子?平衡因子是不是必须的?

首先,解释第一个问题------什么是平衡因子。所谓平衡因子,是针对某个节点来说的,平衡因子的值就是该节点右子树高度 - 左子树高度(或者左子树高度 - 右子树高度)的绝对值。说明一下:下面对于平衡因子的计算,全部采用右子树 - 左子树。

例如上图中的值为2的结点,它的右子树高度为0,左子树高度为1,所以它的平衡因子为-1。

其次,我们来谈一谈为什么要有平衡因子。平衡因子存在的意义就是为了帮助我们判断本次插入是否需要旋转,以确保树的平衡。

例如,新插入的值为0的结点,通过平衡因子的更新,我们可以知道此时需要进行旋转操作了(当某个节点的平衡因子的绝对值超过1时,就需要进行旋转了)。

最后,我们来谈谈平衡因子是不是必须的。并不是的,我们已经知道了平衡因子存在的意义------帮助我们判断是否需要旋转以确保树的平衡。不用平衡因子也可以实现。例如,我们在插入节点0时,通过栈等数据结构保存父节点,通过父节点计算出右子树的高度等方法也是可以的,只是相对比较麻烦。说明一下:本文是需要引入平衡因子(Balance Factor)的。

一棵具有n个节点的AVL树其高度可以保持在(以2为底),搜素的时间复杂度也就是高度次

AVL树的概念及其基本性质我们已经了解了,下面我们开始着手实现,练一练手上的功夫。

AVL树的实现

1、AVL树节点的定义

● 指向左孩子和右孩子的指针。

● 记录该节点平衡因子的bf。

● 指向父节点的指针。

前两点我相信大家都没有疑问,可能有疑问的点在于:为什么需要指向父节点的指针?请看上图,假设值为-1的结点是新插入的结点,那么在插入之前值为1的结点的平衡因子为0。插入值为-1的结点之后,值为1的结点的平衡因子需要更新为-1。新插入的值为0的结点的左右指针均指向空,那么我们如何能找到新插入节点的父节点,然新它的平衡因子呢?这时引入指向父节点的指针就可以帮助我们简单快速的解决这个问题。通过该指针,我们很容易找到每一个节点的父节点,方便插入节点时修改父节点的平衡因子。

cpp 复制代码
template <class T>
struct AVLTreeNode
{
	ALVTreeNode<T>* _left;
	ALVTreeNode<T>* _right;
	ALVTreeNode<T>* _parent;
	int _bf;//平衡因子
	T _data;//结点中存储的数据

	AVLTreeNode(const T& data)
		:_left(nullptr), _right(nullptr), _parent(nullptr)
		,_bf(0), _data(data)
	{}
};

2、AVL树的插入

AVL树本质上还是一棵二叉搜索树,所以无论何种情况,它都必须遵循二叉搜索树的插入规则。再加上AVL树本身特有的平衡因子,我们只需要把它也维护起来即可。下面总结一下:

● 按照二叉搜索树的方式插入新的结点。

● 更新结点的平衡因子。

这两步无论在何种情况下插入都是不可避免的,至于不同情况下的特殊处理,我们接下来慢慢谈。

对于平衡因子,这里采用的是右-左的规则,所以如果在左子树插入,对应的父节点的平衡因子要减1。反之,如果在右子树插入,对应的父节点的平衡因子加1。

插入时的4种情况

①左左: 在左子树根节点(值为30的结点)的左子树上插入节点,进行右单旋。

右单旋的操作就是把subL给提起来,parent自然就会下沉。这样一来,左子树的高度降低,右子树的高度增加,扭转了不平衡的局面。subL变成了根节点,parent变成了subL的右子树,至于subLR,原来是subL的右子树,同时也是parent左子树中的一员,说明subLR比subL大,比parent小,所以将它挂在parent的左子树就很合适。 我们可以看到,在旋转前后,subLR的左右子树没有任何变化,所以subLR的平衡因子在旋转前后不变,parent和subL的平衡因子变为0。当然,这其中还隐藏着一些细节,这些细节都体现在了代码里。

cpp 复制代码
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	//将subLR挂在parent的左子树上
	parent->_left = subLR;

	//如果subLR不为空,则更新它的父节点指针
	if (subLR)			  
		subLR->_parent = parent;

	//更新subL的右孩子
	subL->_right = parent;

	//在更新parent的父节点指针前记录它原来的指向
	Node* parentParent = parent->_parent;

	//更新parent的父节点指针
	parent->_parent = subL;

	//更新subL的父节点指针
	subL->_parent = parentParent;

	//如果parentParent不为空,subL在左子树还是右
	//子树由parent决定,因为subL是来代替parent的
	if (parentParent)
	{
		if (parentParent->_left == parent)
			parentParent->_left = subL;
		else
			parentParent->_right = subL;
	}
	else
	{
		//parentParent为空,说明旋转前
		//parent是根,旋转后要更新根结点
		_root = subL;
		subL->_parent = nullptr;
	}

	//更新平衡因子
	subL->_bf = parent->_bf = 0;
}

②右右:在右子树根节点(值为60的结点)的右子树上插入,左单旋。

左单旋和右单旋是对称的,当我们把subR提起来时,parent就会下沉,进而左子树的高度增加,右子树的高度下降,维持平衡。 具体细节还是在代码里体现更直观一些。左单旋和右单旋这一组,认真理解其中一个后,另一个自然就理解了。

cpp 复制代码
//左单旋的思路和右单旋是一样的,这里我就不过多解释了
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	subR->_left = parent;
	Node* parentParent = parent->_parent;
	parent->_parent = subR;

	subR->_parent = parentParent;

	if (parentParent)
	{
		if (parentParent->_left == parent)
			parentParent->_left = subR;
		else
			parnetParent->_right = subR;
	}
	else
	{
		_root = subR;
		subR->_parent = nullptr;
	}

	subR->_bf = parent->_bf = 0;
}

③在左子树的根节点(值为30的节点)的右子树上插入新节点引起的不平衡,左右双旋,即先左旋再右旋。

左旋:对以subL为根的子树进行左单旋。

右旋:对以parent为根的子树进行右单旋。

这里一定要记住每一次旋转的时候是以谁为根结点的。如果把第一次的左旋理解为对以parent为根的子树进行左旋,那就错了。至于左单旋和右单旋如何操作,我这里就不赘述了,相信看到这里的你一定把前面的单旋都理解了。这里重点关注的对象是平衡因子。

在左子树的根节点(值为30的节点)的右子树上插入新节点引起不平衡的3中情形:

● 当h == 0,60本身就是新增节点。

● 当 h > 0,在b子树插入新增节点引发不平衡。

● 当 h > 0,在c子树插入新增节点引发不平衡。

这三种情形对应subLR的平衡因子有三种情况:0,-1,1。

|-------------|--------------|-----------|-------------|
| 旋转前subLR的bf | 旋转后parent的bf | 旋转subL的bf | 旋转后subLR的bf |
| 0 | 0 | 0 | 0 |
| -1 | 1 | 0 | 0 |
| 1 | 0 | -1 | 0 |

通过这张表,我们看到,每一行的和都为0,这个规律也许能帮我们在不画图的情况下完成bf的调整。

cpp 复制代码
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;//后面根据旋转前subLR的平衡因子来调整,需要记录

	RotateL(subL);
	RotateR(parent);

	if (bf == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0
	}
	else if (bf == 1)
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0
	}
	else
	{
		assert(false);
	}
}

④在右子树的根节点(值为90的节点)的左子树上插入新节点引起的不平衡,右左双旋,即先右旋再左旋。

右左双旋和左右双旋是一对的,分析方法完全一样。

右旋:对以subR为根的子树进行右单旋。

左旋:对以parent为根的子树进行左单旋。

在右子树的根节点(值为90的节点)的左子树上插入新节点引起不平衡的3中情形:

● 当h == 0,60本身就是新增节点。

● 当 h > 0,在b子树插入新增节点引发不平衡。

● 当 h > 0,在c子树插入新增节点引发不平衡。

这三种情形对应subRL的平衡因子有三种情况:0,-1,1。

|-------------|--------------|-----------|-------------|
| 旋转前subRL的bf | 旋转后parent的bf | 旋转subR的bf | 旋转后subLR的bf |
| 0 | 0 | 0 | 0 |
| -1 | 0 | 1 | 0 |
| 1 | -1 | 0 | 0 |

cpp 复制代码
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	RotateR(subR);
	RotateL(parent);

	if (bf == 0)
	{
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else if (bf == 1)
	{
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

有了前面的基础,下面我们来实现insert。

cpp 复制代码
bool Insert(const T& data)
{
	//首次插入
	if (_root == nullptr)
	{
		_root = new Node(data);
		return true;
	}

	Node* cur = _root;
	Node* parent = nullptr;

	//按二叉搜索树的方法插入,先找到位置
	while (cur)
	{
		if (cur->_data < data)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_data > data)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(data);

	//插入
	if (cur->_data < data)
		parent->_right = cur;
	else
		parent->_left = cur;

	cur->_parent = parent;

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

		//如果插入后bf==0,则不需要继续向上调整
		//因为没有改变所在子树的高度
		if (parent->_bf == 0)
			break;

		//改变了所在子树的高度,继续向上调整
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			cur = parent;
			parent = cur->_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
				RotateRL(parent);
		}
		else
		{
			assert(false);
		}
	}
	return true;
}

3、完整代码

cpp 复制代码
#pragma once
#include <assert.h>
#include <iostream>
using namespace std;

template <class T>
struct AVLTreeNode
{
	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	int _bf;//平衡因子
	T _data;//结点中存储的数据

	AVLTreeNode(const T& data)
		:_left(nullptr), _right(nullptr), _parent(nullptr)
		,_bf(0), _data(data)
	{}
};

template <class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:

	bool Insert(const T& data)
	{
		//首次插入
		if (_root == nullptr)
		{
			_root = new Node(data);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;

		//按二叉搜索树的方法插入,先找到位置
		while (cur)
		{
			if (cur->_data < data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_data > data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(data);

		//插入
		if (parent->_data < data)
			parent->_right = cur;
		else
			parent->_left = cur;

		cur->_parent = parent;

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

			//如果插入后bf==0,则不需要继续向上调整
			//因为没有改变所在子树的高度
			if (parent->_bf == 0)
				break;

			//改变了所在子树的高度,继续向上调整
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = cur->_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
					RotateRL(parent);
			}
			else
			{
				assert(false);
			}
		}
		return true;
	}

	void Inorder() { _Inorder(_root); }
private:
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_data << " ";
		_Inorder(root->_right);
	}

	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//将subLR挂在parent的左子树上
		parent->_left = subLR;

		//如果subLR不为空,则更新它的父节点指针
		if (subLR)			  
			subLR->_parent = parent;

		//更新subL的右孩子
		subL->_right = parent;

		//在更新parent的父节点指针前记录它原来的指向
		Node* parentParent = parent->_parent;

		//更新parent的父节点指针
		parent->_parent = subL;

		//更新subL的父节点指针
		subL->_parent = parentParent;

		//如果parentParent不为空,subL在左子树还是右
		//子树由parent决定,因为subL是来代替parent的
		if (parentParent)
		{
			if (parentParent->_left == parent)
				parentParent->_left = subL;
			else
				parentParent->_right = subL;
		}
		else
		{
			//parentParent为空,说明旋转前
			//parent是根,旋转后要更新根结点
			_root = subL;
			subL->_parent = nullptr;
		}

		//更新平衡因子
		subL->_bf = parent->_bf = 0;
	}

	//左单旋的思路和右单旋是一样的,这里我就不过多解释了
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		Node* parentParent = parent->_parent;
		parent->_parent = subR;

		subR->_parent = parentParent;

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

		subR->_bf = parent->_bf = 0;
	}

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;//后面根据旋转前subLR的平衡因子来调整,需要记录

		RotateL(subL);
		RotateR(parent);

		if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(subR);
		RotateL(parent);

		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
private:
	Node* _root = nullptr;
};

#include "ALVTree.h"

void Test()
{
	//int arr1[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int arr1[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	AVLTree<int> t1;
	for (auto val : arr1)
	{
		t1.Insert(val);
	}
	t1.Inorder();
}

int main()
{
	Test();
	return 0;
}

AVL树的性能

由于 AVL 树是一棵平衡二叉搜索树,其每个节点的左右子树的高度差都不超过1,所以 AVL 树是无限接近于满二叉树的。因此AVL 进行查询的时间复杂度就为 O(logN),非常高效;但是如果要对 AVL 树做一些结构修改的操作,其性能就比较低;因为 AVL 树插入时需要调整其达到平衡,那么进行旋转的次数就比较多,更差的是在删除时,有可能要一直让旋转持续到根的位置;因此如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变) 或数据较少进行插入和删除,则可以考虑 AVL 树,但如果一个结构经常进行修改,AVL 则不太适合。


本文到这就结束啦,感谢你的支持!

相关推荐
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
Noah_aa1 小时前
代码随想录算法训练营第五十六天 | 图 | 拓扑排序(BFS)
数据结构
KpLn_HJL2 小时前
leetcode - 2139. Minimum Moves to Reach Target Score
java·数据结构·leetcode
AC使者8 小时前
5820 丰富的周日生活
数据结构·算法
无 证明8 小时前
new 分配空间;引用
数据结构·c++
别NULL13 小时前
机试题——疯长的草
数据结构·c++·算法
ZSYP-S14 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
唐叔在学习14 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA14 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
武昌库里写JAVA17 小时前
浅谈怎样系统的准备前端面试
数据结构·vue.js·spring boot·算法·课程设计