【数据结构】AVL树

目录

前言:

AVL树的定义

AVL树结点的定义

AVL树的插入

AVL树的旋转

左单旋

右单旋

左右双旋

右左双旋

Insert()函数总体代码

AVL树的验证

验证方案一:

验证方案二:


前言:

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下;因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度;

AVL树的定义

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

AVL树结点的定义

cpp 复制代码
//结点类
//k(key) v(value)
template<class k,class v>
struct AVLTreeNode
{
	AVLTreeNode<k, v>* _left;//指向左孩子
	AVLTreeNode<k, v>* _right;//指向右孩子
	AVLTreeNode<k, v>* _parent;//指向父节点
	int _bf = 0;//平衡因子
	pair<k, v> _kv;//有效数据

	//开辟结点时需要构造函数
	AVLTreeNode(const pair<k,v>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
		,_kv(kv)
	{}
};

AVL树的插入

一棵树是AVL树的前提这棵树为搜索二叉树,因此AVL树的插入可以分为如下两步:

  1. 按照二叉搜索树的方式插入新节点
  2. 调整节点的平衡因子
cpp 复制代码
template <class k,class v>
class AVLTree
{
	typedef AVLTreeNode<k, v> Node;
public:
	//首先按照搜索二叉树的规则插入数据
	bool Insert(const pair<k,v>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		//parent记录父节点的位置,cur寻找插入位置
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur != nullptr)
		{
			//根据键值key比较,确定迭代左子树或是右子树
			if ((cur->_kv).first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if ((cur->_kv).first>kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			//为维护搜索二叉树逻辑结构,同一颗树不能出现相等的数值
			else
			{
				return false;
			}
		}
		//开辟结点,存储数据,建立链接关系
		cur = new Node(kv);
		if ((parent->_kv).first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;

        //更新结点的平衡因子
        //......


        return true;
    
    }
private:
	Node* _root = nullptr;
};

**思考:**插入结点后会影响哪些结点的平衡因子呢?

新增节点的部分祖先结点,其中父节点的平衡因子必定发生改变;

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

若新增节点cur插入到parent左侧,那么parent的平衡因子-1;

若新增节点cur插入到parent右侧,那么parent的平衡因子+1;
是否继续更新其余的祖先结点,取决于以parent作为根节点的子树高度是否发生变化

若子树的高度发生变化,则更新爷爷结点的平衡因子;

若子树的高度未发生变化,则不更新爷爷结点的平衡因子;
插入结点后,parent的平衡因子可能出现三种情况:0 正负1 正负2

若插入结点后parent的平衡因子为0,说明插入前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,高度没有发生变化,不再向上更新其余祖先结点;

若插入结点后parent的平衡因子为正负1,说明插入前parent的平衡因子一定为0,插入后被调整成正负1,此时满足AVL树的性质,高度发生变化,需要向上更新其余祖先结点;

插入结点后在cur的迭代过程中其parent的平衡因子为正负2,则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理;

cpp 复制代码
template <class k,class v>
class AVLTree
{
	typedef AVLTreeNode<k, v> Node;
public:
	//首先按照搜索二叉树的规则插入数据
	//其次更新平衡因子bf
	bool Insert(const pair<k,v>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		//parent记录父节点的位置,cur寻找插入位置
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur != nullptr)
		{
			//根据键值key比较,确定迭代左子树或是右子树
			if ((cur->_kv).first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if ((cur->_kv).first>kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			//为维护搜索二叉树逻辑结构,同一颗树不能出现相等的数值
			else
			{
				return false;
			}
		}
		//开辟结点,存储数据,建立链接关系
		cur = new Node(kv);
		if ((parent->_kv).first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		
		cur->_parent = parent;
		//更新平衡因子_bf
		while (parent)
		{
			//更新父节点的平衡因子
			//若cur是parent的左孩子,则parent->_bf--;
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			//若cur是parent的右孩子,则parent->_bf++;
			else
			{
				parent->_bf++;
			}
			//更新祖先结点的平衡因子,根据以parent作为根结点的子树的高度是否发生变化进行更新
			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)
			{
				//迭代过程中以parent作为根结点的子树违背平衡规则---旋转处理
				
			}
			//插入前该树不是AVL树
			else
			{
				assert(false);
			}
		}
		return true;
	}
private:
Node* _root=nullptr;
};

AVL树的旋转

当结点的平衡因子出现正负2时,需要对子树进行旋转处理,旋转的目的首先要维持搜索二叉树的规则,其次要将当前子树由不平衡转化为平衡,最后要降低当前子树的高度;

旋转总共分为四种:左单旋 右单旋 先左单旋后右单旋 先右单旋后左单旋;

左单旋

插入结点后,在cur迭代过程中,出现parent结点的平衡因子为2并且cur的平衡因子为1时诱发左单旋;

cpp 复制代码
    //左单旋
	void RotateLeft(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		Node* ppNode = parent->_parent;

		parent->_right = SubRL;
		parent->_parent = SubR;
		SubR->_left = parent;
		if (SubRL != nullptr)
		{
			SubRL->_parent = parent;
		}
		//当前树为整棵树,更新_root
		if (parent == _root)
		{
			_root = SubR;
			SubR->_parent = nullptr;
		}
		//当前树为某棵树的子树
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = SubR;
			}
			else
			{
				ppNode->_right = SubR;
			}
			SubR->_parent = ppNode;
		}
		//更改平衡因子
		SubR->_bf = 0;
		parent->_bf = 0;
	}

右单旋

cpp 复制代码
    //右单旋
	void RotateRight(Node* parent)
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		Node* ppNode = parent->_parent;
		parent->_left = SubLR;
		parent->_parent = SubL;
		if (SubLR != nullptr)
		{
			SubLR->_parent = parent;
		}
		SubL->_right = parent;
		//当前树为整棵树
		if (ppNode==nullptr)
		{
			_root = SubL;
			SubL->_parent = nullptr;
		}
		//当前树为某棵树的子树
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = SubL;
			}
			else
			{
				ppNode->_right = SubL;
			}
			SubL->_parent = ppNode;
		}

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

插入结点后,在cur迭代过程中,出现parent结点的平衡因子为-2并且cur的平衡因子为-1时诱发右单旋;

左右双旋

当在子树b中插入节点,无论进行左单旋或者右单旋,都不能使得子树达到平衡;

  • 左右双旋情形分析汇总如下

思考:如何调节h=0与h>0(两种情形)情形下SubL、SubLR、parent的平衡因子?

  1. 若SubLR->_bf = -1,则d子树插入节点;

  2. 若SubLR->_bf = 1,则e子树插入节点;

  3. 若SubLR->_bf = 0,则SubLR本身即为新增节点;

注:双旋中的单旋会更改SubLR的平衡因子,插入结点后应记录旋转前SubLR的平衡因子;

cpp 复制代码
    //先左单旋后右单旋
	//根据SubLR的平衡因子调节三种情形下SubL parent SubLR的平衡因子
	void RotateLR(Node* parent)
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		int bf = SubLR->_bf;
		//双旋中的单旋会更改平衡因子,应记录旋转之前SubLR中的平衡因子
		RotateLeft(parent->_left);
		RotateRight(parent);
		//SubLR本身为新增结点
		if (bf == 0)
		{
			parent->_bf = 0;
			SubLR->_bf = 0;
			SubL->_bf = 0;
		}
		//子树b被拆解的左子树d插入结点
		else if (bf == -1)
		{
			parent->_bf = 1;
			SubLR->_bf = 0;
			SubL->_bf = 0;
		}
		//子树b被拆解的右子树e插入结点
		else if (bf == 1)
		{
			parent->_bf = 0;
			SubLR->_bf = 0;
			SubL->_bf = -1;
		}
		else
		{
			assert(false);
		}
	}

右左双旋

cpp 复制代码
    //先右单旋后左单旋
	void RotateRL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		int bf = SubRL->_bf;

		RotateRight(SubR);
		RotateLeft(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 pair<k,v>& kv)
{
if (_root == nullptr)
{
	_root = new Node(kv);
	return true;
}
//parent记录父节点的位置,cur寻找插入位置
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr)
{
	//根据键值key比较,确定迭代左子树或是右子树
	if ((cur->_kv).first < kv.first)
	{
		parent = cur;
		cur = cur->_right;
	}
	else if ((cur->_kv).first>kv.first)
	{
		parent = cur;
		cur = cur->_left;
	}
	//为维护搜索二叉树逻辑结构,同一颗树不能出现相等的数值
	else
	{
		return false;
	}
}
//开辟结点,存储数据,建立链接关系
cur = new Node(kv);
if ((parent->_kv).first > kv.first)
{
	parent->_left = cur;
}
else
{
	parent->_right = cur;
}
		
cur->_parent = parent;
//更新平衡因子_bf
while (parent)
{
	//更新父节点的平衡因子
	//若cur是parent的左孩子,则parent->_bf--;
	if (cur == parent->_left)
	{
		parent->_bf--;
	}
	//若cur是parent的右孩子孩子,则parent->_bf++;
	else
	{
		parent->_bf++;
	}
	//更新祖先结点的平衡因子,根据以parent作为根结点的子树的高度是否发生变化进行更新
	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)
	{
		//迭代过程中以parent作为根结点的子树违背平衡规则---旋转处理
		//左单旋场景
		if (parent->_bf == 2 && cur->_bf == 1)
		{
			RotateLeft(parent);
		}
		//右单旋场景
		else if (parent->_bf == -2 && cur->_bf == -1)
		{
			RotateRight(parent);
		}
		//先左旋后右旋场景
		else if (parent->_bf == -2 && cur->_bf == 1)
		{
			RotateLR(parent);
		}
		//先右旋后左旋场景
		else
		{
			RotateRL(parent);
		}
		break;
	}
	//插入前该树不是AVL树
	else
	{
		assert(false);
	}
	}
return true;
}

AVL树的验证

AVL树的验证:

  1. 首先检查其是否为搜索二叉树---利用二叉树的中序遍历得到有序序列验证;
  2. 求取每个结点子树的高度并计算左右子树高度差,验证高度差的绝对值小于2;
  3. 验证平衡因子是否计算正确;
cpp 复制代码
//中序遍历
void InOrder()
{
	_InOrder(_root);
}
//中序遍历子函数
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;
	_InOrder(root->_left);
	cout << (root->_kv).first << ":" << (root->_kv).second << endl;
	_InOrder(root->_right);
}

中序遍历结果为有序只能证明为搜索二叉树,要证明二叉树是AVL树还需验证二叉树的平衡性,在此过程中可以检查每个结点的平衡因子是否计算正确;

验证方案一:

cpp 复制代码
    //求取当前树的高度
	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;
	}

    //前序遍历验证其是否为平衡树,验证根时求取右子树与左子树的高度并判断平衡因子是否计算正确
    bool _IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;
		//求取左子树,右子树的高度
		int LeftHeight = Height(root->_left);
		int RightHeight = Height(root->_right);
		//判断当前树是否为平衡树
		if (abs(RightHeight - LeftHeight) >= 2)
		{
			cout << (root->_kv).first << ":" << (root->_kv).second << "不平衡" << endl;
			return false;
		}
		//判断当前根结点处的平衡因子是否正常
		if ((RightHeight - LeftHeight)!=root->_bf)
		{
			cout << (root->_kv).first << ":" << (root->_kv).second << "平衡因子异常" << endl;
			return false;
		}
		return _IsBalance(root->_left) && _IsBalance(root->_right);
	}

采用前序遍历验证其是否为平衡树,验证根时求取右子树与左子树的高度,当验证下一层又要求下一层左子树与右子树的高度,并未保留上一次计算时的数据,存在大量冗余的计算,导致效率低下;

验证方案二:

采用后序遍历验证其是否为平衡树,验证步骤如下:

  1. 从叶子结点处开始计算每课子树的高度(每棵子树的高度 = 左右子树中高度的较大值 + 1);
  2. 先判断左子树是否是平衡二叉树;
  3. 再判断右子树是否是平衡二叉树;
  4. 若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止;(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树)
cpp 复制代码
    bool IsBalance()
	{
		int height = 0;
		return _IsBalance(_root, height);
	}
    bool _IsBalance(Node* root, int& height)
	{
		if (root == nullptr)
		{
			height = 0;
			return true;
		}

		int leftHeight = 0, rightHeight = 0;
		if (!_IsBalance(root->_left, leftHeight)
			|| !_IsBalance(root->_right, rightHeight))
		{
			return false;
		}

		if (abs(rightHeight - leftHeight) >= 2)
		{
			cout << root->_kv.first << "不平衡" << endl;
			return false;
		}

		if (rightHeight - leftHeight != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}

		height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;

		return true;
	}

欢迎大家批评指正,博主会持续输出优质内容,谢谢各位观众老爷观看,码字画图不易,希望大家给个一键三连支持~ 你的支持是我创作的不竭动力~

相关推荐
忍界英雄3 分钟前
LeetCode:2398. 预算内的最多机器人数目 双指针+单调队列,时间复杂度O(n)
算法·leetcode·机器人
Kenneth風车4 分钟前
【机器学习(五)】分类和回归任务-AdaBoost算法-Sentosa_DSML社区版
人工智能·算法·低代码·机器学习·数据分析
C7211BA22 分钟前
使用knn算法对iris数据集进行分类
算法·分类·数据挖掘
Tisfy24 分钟前
LeetCode 2398.预算内的最多机器人数目:滑动窗口+单调队列——思路清晰的一篇题解
算法·leetcode·机器人·题解·滑动窗口
程序猿练习生28 分钟前
C++速通LeetCode简单第18题-杨辉三角(全网唯一递归法)
c++·算法·leetcode
Huazzi.32 分钟前
算法题解:斐波那契数列(C语言)
c语言·开发语言·算法
汉字萌萌哒33 分钟前
【2022 CCF 非专业级别软件能力认证第一轮(CSP-J1)入门级 C++语言试题及解析】
数据结构·c++·算法
2301_8071805434 分钟前
icpc江西:L. campus(dij最短路)
算法
th新港34 分钟前
CCF201909_1
数据结构·c++·算法·ccf
Dola_Pan36 分钟前
字符串的KMP算法详解及C/C++代码实现
算法