数据结构:探秘AVL树

本节重点

  • 理解AVL树的概念
  • 掌握AVL树正确的插入方法
  • 利用_parent指针正确更新平衡因子
  • 掌握并理解四种旋转方式:左单旋,右单旋,左右双旋,右左双旋

一、AVL树的概念

AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

AVL树最先发明自平衡二叉搜索树,AVL是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。

AVL树的实现引入一个平衡因子的概念(balance factor)的概念,每个节点都有一个平衡因子,任何节点的平衡因子等于右子树高度减去左子树高度,也就是说任何节点的平衡因子等于0/1/-1,AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。

AVL树整体的节点数量和分布和完全二叉树类似,但是AVL树高度可以控制在logN,所以增删查改的效率也可以控制在O(logN),相比二叉搜索树有了本质的提升。

如图,每个节点上方的小数字表示该节点的平衡因子,平衡因子只能为-1/1/0当为2/-2时我们要通过旋转将左右子树重新达到平衡状态。

二、AVL树的实现

2.1 AVL树的结构

AVL树我们分成两部分来实现,一部分是单个节点的定义,一部分是AVL树。并且用两个类进行封装:

cpp 复制代码
template<class K>
struct AVLTNode//AVL树节点的定义
{
	K _key;
	struct AVLTNode<K>* _left;
	struct AVLTNode<K>* _right;

	struct AVLTNode<K>* _parent;//引入parent方便我们快速向上更新平衡因子

	int _bf;//平衡因子(balance factor)

	//构造函数:
	AVLTNode(K key)
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}
};


//AVL树的定义
template<class K>
class AVLTree
{
	typedef struct AVLTNode<K> Node;
public:
private:
	Node* root = nullptr;
};

2.2 AVL树的插入

AVL树插入的步骤:

  1. 插入一个值按照二叉搜索树的规则插入
  2. 新增节点后,只会影响祖先节点的高度,也就是可能会影响部分祖先节点的平衡因子,所以更新从新增节点->根节点路径上的平衡因子,实际最坏情况下更新到根,有些情况更新到中间就停止了。
  3. 更新平衡因子过程中没有出现问题,则插入结束
  4. 更新平衡因子的过程中出现不平衡,对不平衡子树旋转,旋转后降低了子树的高度,不会再影响上一层,所以插入结束

2.2.1 先按照搜索二叉树规则插入

在插入之前,我们需要判断该AVL树是否为空,若为空直接在_root 新增节点并返回,若不为空说明AVL树中已经存在节点,这时我们需要从根节点开始依次按照搜索树的规则寻找插入位置,找到之后创建新节点并链入到AVL树中。

代码示例:

cpp 复制代码
bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			//AVL是一颗空树
			_root=new Node(key)
            return 1;
		}
		else
		{
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					assert(false);
				}
			}
			//链接新节点:
			cur = new Node(key);
			if (cur->_key > parent->_key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			cur->_parent = parent;

		}

2.2.2 判断并更新平衡因子

更新规则:

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

插入节点会增加子树高度,影响当前节点平衡因子因为一次只能插入一个节点所以平衡因子要么++要么--:

插入在右子树平衡因子++;插入在左子树平衡因子--。

平衡因子的三种情况:(0,-1/1,-2/2)

1、更新后parent节点平衡因子为0

说明更新之前parent节点平衡因子为1或-1也就是左右子树一边高一边低,节点插入在低的一边,插入后左右平衡不会影响上一级节点的平衡因子。

2、更新后parent节点平衡因子为1/-1

说明更新之前parent节点平衡因子为0,插入后左右子树一边高一边低会影响上一级节点的平衡因子,所以要继续向上更新。

3、更新后parent节点平衡因子为2/-2

说明更新之前parent节点平衡因子为1/-1也就是左右子树一边高一边低,节点插入在高的一边

代码示例:

cpp 复制代码
while (parent)
{
	if (parent->_right == cur)
	{
		parent->_bf++;
	}
	else
	{
		parent->_bf--;
	}
	if (parent->_bf == 0)
	{
		//说明新增节点在低的一边,插入后左右子树平衡
		//不会影响祖先节点的 _bf直接break
		break;
	}
	else if(parent->_bf==1||parent->_bf==-1)
	{
		//新增节点之后为1或-1说明之前为0(左右子树平衡)
		//此时需要更新依次更新祖先节点的 _bf直到更新到根节点或某一祖先节点_bf==0
		cur = parent;
		parent = parent->_parent;
	}
	else if (parent->_bf == 2 || parent->_bf == -2)
	{
		if (parent->_bf == 2 && cur->_bf == 1)
		{
			//单纯右边高,左旋
			RotateL(parent);
		}
		else if (parent->_bf == -2 && cur->_bf == -1)
		{
			//单纯左边高,右旋
			RotateR(parent);
		}
		else if (parent->_bf == 2 && cur->_bf == -1)
		{
			//先左后右,右左双旋
			RotateRL(parent);
		}
		else if (parent->_bf == -2 && cur->_bf == 1)
		{
			//先右后左,左右双旋
			RotateLR(parent);
		}
		else
		{
			assert(false);
		}

		break;
	}
    else
    {
        assert(false);
    }
}

2.2.3 (不平衡)旋转子树

旋转的原则:

  • 保持搜索树的规则
  • 让旋转的树从不满足变为平衡,其次降低树的高度

首先我们需要明白的是旋转操作分为两部分,一是调整节点之间的链接关系,二是更新平衡因子 _bf

左单旋(RotateL)

当parent的平衡因子为2,且cur的平衡因子为1时AVL树会呈现右子树一边高的形式,这时我们需要进行左旋操作,需要注意的是满足 parent->_bf==2 && cur->_bf==1 条件的AVL树的形式可能有多种:

为了便于理解,我们可以选择一种简单的情况进行分析并写出相应代码:

代码示例:

cpp 复制代码
void RotateL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		Node* pparent = parent->_parent;
		if (SubRL)
		{
			SubRL->_parent = parent;
		}
		parent->_right = SubRL;
		SubR->_left = parent;
		parent->_parent = SubR;
		if (parent == _root)
		{
			_root = SubR;
			SubR->_parent = nullptr;
		}
		else
		{
			if (pparent->_right == parent)
			{
				pparent->_right = SubR;
			}
			else
			{
				pparent->_left = SubR;
			}
			SubR->_parent = pparent;
		}
		//节点链接关系调整完成后更新平衡因子:
		SubR->_bf = 0;
		parent->_bf = 0;
	}
右单旋(RotateR)

类似的是,满足 parent->_bf==-2 && cur->_bf==-1 条件的AVL树的形式也可能有多种

我们也选择其中一种简单的情况进行分析和编写代码:

cpp 复制代码
void RotateR(Node* parent)
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		Node* pparent = parent->_parent;
		if (SubLR)
		{
			SubLR->_parent = parent;
		}
		parent->_left = SubLR;
		SubL->_right = parent;
		parent->_parent = SubL;
		if (parent == _root)
		{
			_root = SubL;
			SubL->_parent = nullptr;
		}
		else
		{
			if (pparent->_right == parent)
			{
				pparent->_right = SubL;
			}
			else
			{
				pparent->_left = SubL;
			}
			SubR->_parent = pparent;
		}
		//节点链接关系调整完成后更新平衡因子:
		SubL->_bf = 0;
		parent->_bf = 0;
	}
左右双旋(RotateLR)

与单旋不同的是,双旋对应的AVL树的结构不再是单纯一边高,我们由条件判断很容易就可以看出来(parent->_bf == -2 && cur->_bf == 1 或 parent->_bf == 2 && cur->_bf == -1),此时我们发现AVL树节点类似"折线形"排列,这时单纯的单旋无法使二叉树再次平衡,我们需要进行两次单旋来解决。

类似的是满足双旋触发条件时,AVL树的结构要是拓展开来也有非常多种情况,我们可以选择其中一种较为简单的情况来分析并编写相应代码:

需要注意的是左右双旋的时候对SubLR的_bf也要进行考虑,目的是确定SubLR是否存在单个的子树,因为最终SubLR的子树会链入SubL或者parent影响两个节点的平衡因子。

cpp 复制代码
void RotateLR(Node* parent)
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		int bf = SubLR->_bf;
		RotateL(parent->_left);
		RotateR(parent);
		if (bf == 1)
		{
			SubLR->_bf = 0;
			parent->_bf = 0;
			SubL->_bf = -1;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			SubLR->_bf = 0;
			SubL->_bf = 0;
		}
		else
		{
			SubLR->_bf = 0;
			parent->_bf = 1;
			SubL->_bf = 0;
		}
	}
右左双旋(RotateRL)

cpp 复制代码
void RotateRL(Node* parent)
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		int bf = SubRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		if (bf == 1)
		{
			SubRL->_bf = 0;
			parent->_bf = -1;
			SubR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			SubLR->_bf = 0;
			SubL->_bf = 0;
		}
		else
		{
			SubRL->_bf = 0;
			parent->_bf = 0;
			SubR->_bf = 1;
		}
	}
相关推荐
UP_Continue3 小时前
排序--归并排序
数据结构
飞鼠_3 小时前
详解数据结构之树、二叉树、二叉搜索树详解 C++实现
开发语言·数据结构·c++
丶Darling.4 小时前
26考研 | 王道 |数据结构 | 第二章 线性表
数据结构·考研
学也不会4 小时前
d2025331
java·数据结构·算法
Aurora_wmroy5 小时前
算法竞赛备赛——【数据结构】并查集
数据结构·c++·算法·蓝桥杯
pystraf6 小时前
P8310 〈 TREEのOI 2022 Spring 〉Essential Operations Solution
数据结构·c++·算法·线段树·洛谷
·醉挽清风·6 小时前
学习笔记—数据结构—二叉树(链式)
c语言·数据结构·c++·笔记·学习·算法
Susea&6 小时前
数据结构初阶:单链表
c语言·开发语言·数据结构
Wo3Shi4七7 小时前
二叉树数组表示
数据结构·后端·go
刃神太酷啦8 小时前
基础算法篇(3)(蓝桥杯常考点)-图论
数据结构·c++·算法·职场和发展·蓝桥杯·图论·蓝桥杯c++组