C++高级数据结构算法 | AVL(自平衡二叉查找树)

版权声明:本文为博主整理文章,未经博主允许不得转载。 https://blog.csdn.net/ZYZMZM_/article/details/90670480

之前我们向大家介绍了 高级数据结构算法中的 BST 树,即二叉查找树:
《C++高级数据结构算法 | Binary Search Tree(二叉查找树)》
我们使用递归和非递归的方式实现了BST树的插入、删除、查询、四种遍历等操作,也对BST树的相关典型题目做了分析和代码实现。

本篇博文将讲解AVL树的基本概念和相关操作与题目分析。



AVL树的引入

通过之前的讲解我们知道,二叉查找树是基于折半查找思想设计的一种数据结构。通过分析,二叉查找树的确能在很大程度上提高查找的效率。然而,尽管当二叉查找树处于平衡状态时,其操作的时间复杂度为 O ( l o g 2 n ) O(log_2n) ,但是当二叉查找树是单支树时,其搜索效率将为 O ( n ) O(n) ,将退化成为一条链表,白白浪费掉另一个结点域,如下图所示

基于上述问题,我们可以发现,二叉搜索树的平衡性是影响其操作效率的关键,因此学者们设计了第一个平衡二叉搜索树,即AVL树,AVL树得名于它的发明者前苏联数学家格奥尔吉·阿杰尔松-韦利斯基 ( G . M . A d e l s o n V e l s k y ) (G. M. Adelson-Velsky) 和 叶夫吉尼·兰迪斯 ( E . M . L a n d i s ) (E. M. Landis)


AVL树的概念

AVL树就是一棵二叉查找树,其准确的定义如下:一棵AVL树或者是空树,或者是具有下列性质的二叉查找树——它的左子树和右子树都是AVL树,且左子树和右子树的高度之差的绝对值不超过 1 1 。也就是说,AVL树本质上是带了平衡功能的二叉查找树。下图即为一棵AVL树,树中略去了各结点的关键码。

注意:AVL树的平衡性是一种相对的平衡,而非一种绝对的平衡。与绝对平衡相比,这种所谓的相对平衡满足的是一个较弱的平衡条件,即它不要求左子树和右子树的高度完全相等,而仅仅是左子树和右子树的高度之差的绝对值不超过 1 1 即可。之所以将平衡条件降低是因为绝对的平衡很难实现。

结点的平衡因子是它的左子树的高度减去它的右子树的高度(可也为右子树的高度减去它的左子树的高度)。带有平衡因子 1、0 或 -1 的节点被认为是平衡的。平衡因子的绝对值大于 1 的结点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个结点中,或从可能存储在结点中的子树高度计算出来。

对于有n个结点的AVL树,其高度可保持在 l o g 2 n \lfloor log_2n \rfloor 左右,其查找、插入和删除在平均和最坏情况下都是 O ( l o g 2 n ) O(log_2n) 增加和删除可能需要通过一次或多次树旋转来重新平衡这个树


AVL树的结构定义

我们知道AVL树就是一棵二叉查找树,因此依旧采用二叉链表的方式进行结构的定义,与普通二叉查找树不同的是,我们需要为每个结点添加一个结点高度域,负责存储该结点的高度,因此我们有需要提供两个API接口负责返回该结点的高度及计算该结点左右子树的高度差。

template<typename T>
class AVL
{
public:
	AVL() { _root = nullptr; }
	···
private:
	struct AVLNode
	{
		AVLNode(T data = T())
			:_data(data)
			, _left(nullptr)
			, _right(nullptr)
			, _height(1) 
		{}
		T _data;
		AVLNode *_left;
		AVLNode *_right;
		int _height; // 存储的就是节点的高度
	};

	// 返回节点的高度
	int height(AVLNode *node)const
	{
		return node == nullptr ? 0 : node->_height;
	}

	// 返回左右子树最高的层数
	int maxHeight(AVLNode *node1, AVLNode *node2)
	{
		return height(node1) > height(node2) 
		? height(node1) : height(node2);
	}
	
	AVLNode *_root;
};

接下来我们简单分析一下下图中的插入操作:

如上图,左边是一棵AVL树,它是平衡的。当我们给 结点7 插入左孩子时,AVL的平衡特性就被破坏,由于 结点8 失衡了,即 结点8 的左右子树高度差为 2,不满足平衡条件。

再例如,我们将 结点1 删除,此时左边的AVL树的平衡特性也被破坏了,结点2失衡。

我们知道,AVL是平衡的二叉查找树,因此AVL树的基本操作与普通的二叉查找树是相似的,但是我们经过上述分析发现,在我们进行结点的插入和删除时,有可能会破坏AVL树的平衡性,实时的保持这种平衡性非常重要。通常可以通过调整树结构,使之保持平衡,这种用以进行平衡化的操作被称为 旋转

接下来我们具体分析AVL树的四种旋转算法并实现。


AVL树的旋转算法

如果在一棵平衡的二叉排序树中插入一个新的结点,就可能造成其失衡,这种失衡可能归结为四种基本情况。

上述的四种失衡情况相对应的处理方式被分为两类:单旋和双旋。其中,单旋又分为左旋和右旋,而双旋又分为先左后右和先右后左两种

通常每次向AVL树中插入一个新节点时,AVL树中相关结点的平衡状态就会发生改变。因此,在插入一个新节点后,就需要从插入位置沿通向根的路径回溯,检查各结点的左右子树高度差。如果在某一点发现高度不平衡,则停止回溯,从发生不平衡的节点起,沿刚才回溯的路径取直接下两层的节点。这时就有两种情况,

  • 如果这三个节点处在同一条直线上,那么采用单旋进行平衡化
  • 如果这三个节点不处于同一条直线上,那么采用双旋进行平衡化

具体采用的操作如下表所示:

插入方式 描述 旋转方式
LL 在某节点的左子树中插入一个左孩子 右旋转
RR 在某节点的右子树中插入一个右孩子 左旋转
LR 在某节点的左子树中插入一个右孩子 先左旋后右旋
RL 在某节点的右子树中插入一个左孩子 先右旋后左旋

接下来我们具体讲解一下它们的这四类旋转算法及其代码实现。


右单旋转

由于向某节点的左子树中插入一个左孩子导致该结点失衡,我们需要使用右旋操作来维护AVL树的平衡。
在这里插入图片描述
下图是具体情境分析:

右单旋转的方法是以3个成直线排列的节点中的的中间节点为轴,进行顺时针旋转该中间节点的原父节点变成该节点的右子节点,该中间节点的右子树则变成其原父节点的左子树

// 右旋转操作
AVLNode* rightRotate(AVLNode* node)
{
	AVLNode* child = node->_left; // 拿到中间节点
	node->_left = child->_right; //该中间节点的右子树则变成其原父节点的左子树
	child->_right = node;// 该中间节点的原父节点变成该节点的右子节点
	node->_height = maxHeight(node->_left, node->_right) + 1; // 更新节点高度
	child->_height = maxHeight(child->_left, child->_right) + 1;// 更新节点高度
	return child;//返回旋转后的根节点
}

左单旋转

由于向某节点的右子树中插入一个右孩子导致该结点失衡,我们需要使用左旋操作来维护AVL树的平衡。
在这里插入图片描述
下图是具体情境分析:

左单旋转的方法是以3个成直线排列的节点的中间节点为轴,进行逆时针旋转该中间节点的原父节点变成该节点的左子节点,该中间节点的左子树则变成其原父节点的右子树。

// 左旋转操作 以node为根节点进行左旋转,返回旋转后的根节点
AVLNode* leftRotate(AVLNode* node)
{
	AVLNode* child = node->_right;// 拿到中间节点
	node->_right = child->_left;// 该中间节点的左子树则变成其原父节点的右子树
	child->_left = node;// 该中间节点的原父节点变成该节点的左子节点
	node->_height = maxHeight(node->_left, node->_right) + 1;// 更新节点高度
	child->_height = maxHeight(child->_left, child->_right) + 1;// 更新节点高度
	return child; //返回旋转后的根节点
}

上面我们介绍了AVL树的两种单向旋转方式。那么接下来我们首先看一下如下图所示的这类情况。显然经过一次单旋转的修复后无论是X或者W作为根结点都无法符合AVL树的性质,此时就需要用双旋转算法来实现了。

由于子树Y是在插入某个结点后导致X结点的左右子树失去平衡,那么就说明子树Y肯定是非空的,因此为了易于理解,我们可以把子树Y看作一个根结点和两棵子树,如下图所示:

下面我们就来讲解双旋转算法。


左-右双旋转

由于向某节点的左子树中插入一个右孩子导致该结点失衡,我们需要使用先左后右双向旋转操作来维护AVL树的平衡。

先左后右双旋转的处理方法是以3个成折线排列的节点中的末节点为轴,进行逆时针旋转(左旋),使得末节点代替中间节点的位置,也就是让末节点成为原中间节点的父节点,而末节点的左子树变为原中间节点的右子树。这时,这3个节点将成一直线排列,原来的末节点变成了3条直线的中间节点,而原来的中间节点变成了3条直线中的末节点。这时再以新的中间节点为旋转轴做右单旋转,即可完成平衡操作。

简而言之:首先对原失衡节点的左子树进行左旋操作,再对原失衡节点做右旋操作即可。

// 左平衡  左-右旋转
AVLNode* leftBalance(AVLNode* node)
{
	node->_left = leftRotate(node->_left);
	return rightRotate(node);
}

右-左双旋转

由于向某节点的右子树中插入一个左孩子导致该结点失衡,我们需要使用先右后左双向旋转操作来维护AVL树的平衡。

先右后左双旋转的处理方法是以3个成折线排列的节点中的末节点为轴,进行顺时针旋转(右旋),使得末节点代替中间节点的位置,也就是让末节点成为原中间节点的父节点,而末节点的右子树变为原中间节点的左子树。这时,这3个节点将成一直线排列,原来的末节点变成了3条直线的中间节点,而原来的中间节点变成了3条直线中的末节点。这时再以新的中间节点为旋转轴做左单旋转,即可完成平衡操作。

简而言之:首先对原失衡节点的右子树进行右旋操作,再对原失衡节点做左旋操作即可。

// 右平衡  右-左旋转
AVLNode* rightBalance(AVLNode* node)
{
	node->_right = rightRotate(node->_right);
	return leftRotate(node);
}

AVL树的插入和删除

AVL树的插入

/**
 * AVL树的插入操作与普通的二叉查找树基本类似,区别就是在我们每次插入了新节点后
 * 能会导致AVL树失去平衡特性,我们使用递归回溯的特性,每当创建好新节点回溯到父
 * 节点插入后,判断该结点的左右子树高度差,然后进行相应操作。
 * 具体的实现是我们给某结点的左子树插入新节点后,那么该结点的左子树高度便增加1,
 * 此时有可能失衡,但是我们需要判断是由于向左子树插入左孩子导致失衡(需要右旋)
 * 还是向左子树插入右孩子导致失衡(需要左-右旋转)。判断方法就是判断当前结点
 * 与插入的val的大小关系,大于当前结点肯定是插到了当前结点的右边,小于当前结
 * 点肯定是插入到了当前结点的左边,然后进行相应的旋转操作即可。
 * 那么对于在某节点的右子树插入新节点的情况与上边的分析过程类似,这里不再赘述。
 */
void insert(const T& val)
{
	_root = insert(_root, val);
}
AVLNode* insert(AVLNode* node, const T& val)
{
	if (node == nullptr)
	{
		return new AVLNode(val);
	}

	if (val < node->_data)
	{
		// 回溯到父节点插入
		node->_left = insert(node->_left, val);
		// 插入左子树 做AVL旋转操作
		if (height(node->_left) - height(node->_right) > 1)
		{
			// 左孩子的左子树失衡 右旋转操作
			if (node->_data > val)
			{
				node = rightRotate(node);
			}
			else // 左孩子的右子树失衡 左-右旋转操作
			{
				node = leftBalance(node);
			}
		}
	}
	else if (val > node->_data)
	{
		node->_right = insert(node->_right, val);
		// 插入左子树 做AVL旋转操作
		if (height(node->_right) - height(node->_left) > 1)
		{
			// 右孩子的的右子树失衡  左旋转操作
			if (node->_data < val)
			{
				node = leftRotate(node);
			}
			else // 右孩子的左子树失衡 右-左旋转操作
			{
				node = rightBalance(node);
			}
		}
	}
	else
	{
		;
	}
	// 更新节点高度
	node->_height = maxHeight(node->_left, node->_right) + 1;
	return node;
}

AVL树的删除

/**
 * AVL树的删除操作基本框架和普通的二叉查找树也是类似的,我们在分析BST树的删除
 * 情况时分了三种情况,在AVL树中同样是适用的,但是对于第三种情况,也就是待删除
 * 结点同时拥有左右孩子的情况,我们之前讲解的是可以使用前驱或后继节点来替代当前
 * 节点并将其前驱或后继删除,我们只采用了其中一种方法,但是在AVL树的删除中,有
 * 可能导致失衡从而需要使用旋转操作来维护平衡,但是在这里直接判断当前待删除结点
 * 的左右子树高度,若左子树高我们删除前驱,若右子树高或者高度相同我们删除后继,
 * 这样就不用进行旋转操作,简化编程流程。
 * 但是对于普通的情况,即待删除结点只有一个孩子的情况,我们就必须要进行相应判断
 * 和旋转操作来维护AVL树的平衡特性。
 * 如果我们待删除的结点在左子树中找到,那么删除后,左子树高度降低,那么失衡肯定
 * 是由于右子树的高度过高导致的。因此我们直接判断右子树的右孩子与右子树的左孩子
 * 的高度大小,若右子树的右孩子高度更高,那么我们进行左旋操作维护平衡,否则我们
 * 进行右-左双旋转维护平衡。
 * 如果我们待删除的结点在右子树中找到,分析过程与上述类似,这里不再赘述。
 */
void remove(const T& val)
{
	_root = remove(_root, val);
}
AVLNode* remove(AVLNode* node, const T& val)
{
	if (node == nullptr)
	{
		return nullptr;
	}
	
	if (node->_data > val)
	{
		node->_left = remove(node->_left, val);
		if (height(node->_right) - height(node->_left) > 1)
		{
			if (height(node->_right->_right) 
			> height(node->_right->_left))
			{
				node = leftRotate(node);
			}
			else
			{
				node = rightBalance(node);
			}
		}

	}
	else if (node->_data < val)
	{
		node->_right = remove(node->_right, val);
		if (height(node->_left) - height(node->_right) > 1)
		{
			if (height(node->_left->_left) 
			>= height(node->_left->_right))
			{
				node = rightRotate(node);
			}
			else
			{
				node = leftBalance(node);
			}
		}
	}
	else
	{
		if (node->_left != nullptr && node->_right != nullptr)
		{
			if (height(node->_left) > height(node->_right))
			{
				AVLNode* preNode = node->_left;
				while (preNode->_right != nullptr)
				{
					preNode = preNode->_right;
				}
				node->_data = preNode->_data;
				node->_left = remove(node->_left, preNode->_data);
			}
			else
			{
				AVLNode* lastNode = node->_right;
				while (lastNode->_left != nullptr)
				{
					lastNode = lastNode->_left;
				}
				node->_data = lastNode->_data;
				node->_right = remove(node->_right, lastNode->_data);
			}
		}
		else if (node->_left != nullptr)
		{
			AVLNode* child = node->_left;
			delete node;
			//node->_height = maxHeight(node->_left, node->_right) + 1;
			return child;
		}
		else if (node->_right != nullptr)
		{
			AVLNode* child = node->_right;
			delete node;
			//node->_height = maxHeight(node->_left, node->_right) + 1;
			return child;
		}
		else
		{
			delete node;
			//node->_height = maxHeight(node->_left, node->_right) + 1;
			return nullptr;
		}
	}
	node->_height = maxHeight(node->_left, node->_right) + 1;
	return node;
}

判断一棵二叉搜索树是否是平衡树

/**
 * 判断一棵二叉搜索树是否是平衡树,因为题目条件已经说明了该树是一棵二叉搜索树
 * 了,因此我们直接从二叉搜索树与平衡二叉搜索树在性质上的重要区别入手,即一颗
 * 平衡二叉搜索树任一结点的左右子树高度差不超过1,因此我们借助了求层数的函数
 * level(),在函数递归前判断是否满足该条件即可,若不满足,我们直接结束,若满足
 * 继续递归遍历其他结点即可。
 */
bool isAVL()
{
	return isAVL(_root);
}
bool isAVL(AVLNode *node)
{
	if (node == nullptr)
	{
		return true;
	}

	if (abs(level(node->_left) - level(node->_right)) > 1)
	{
		return false;
	}
	return isAVL(node->_left) && isAVL(node->_right);
}

/**
 * 上边函数所用到的求树层数的函数level()的定义如下
 */
int level()
{
	return level(_root);
}

int level(AVLNode* node)
{
	if (node == nullptr)
	{
		return 0;
	}

	int left = level(node->_left);
	int right = level(node->_right);

	return (left > right ? left : right) + 1;
}

判断一棵二叉树是否是平衡二叉搜索树

/**
 * 判断一颗二叉树是否是平衡二叉搜索树,我们之前有写过判断一颗二叉树是否是
 * 二叉搜索树(BST)的代码,我们在递归函数前进行很多的条件判断,那么我们
 * 只需要在这部分继续添加条件,判断是否是平衡树即可。判断方式和上题是
 * 相同的。
 */
bool isAVLTree()
{
	return isAVLTree(_root);
}
bool isAVLTree(AVLNode* node)
{
	static AVLNode* prev = nullptr;
	if (node == nullptr)
	{
		return true;
	}

	if (!isAVLTree(node->_left))
	{
		return false;
	}

	if (prev != nullptr && node->_data < prev->_data)
	{
		return false;
	}

	if (abs(level(node->_left) - level(node->_right)) > 1)
	{
		return false;
	}

	prev = node;
	return isAVLTree(node->_right);
}

AVL树的其他操作:查找、遍历等都与BST二叉查找树是相同的,包括其他算法面试题目,大家可以参考我之前的博文:
《C++高级数据结构算法 | Binary Search Tree(二叉查找树)》

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/90670480