【C++进阶】AVL树

在这里插入图片描述

个人主页:@Weraphael
✍作者简介:目前学习C++和算法
✈️专栏:C++航路
希望大家多多支持,咱一起进步!
如果文章对你有帮助的话
欢迎 评论 点赞 收藏 加关注✨


前言

在搜索二叉树章节,我们知道二叉搜索树可能会失去平衡(退化成单支树),造成搜索效率低落的情况,时间复杂度会退化成O(N)(效率没有保障)。当然为了避免这种情况,可以使用平衡二叉树,例如AVL树或红黑树等。
【C++进阶】AVL树_第1张图片

目录

  • 前言
  • 一、AVL树的概念
  • 二、AVL树的定义
  • 三、如何更新平衡因子
  • 四、如何控制平衡
        • 解答为需要定义parent指针变量
  • 五、插入操作(重点)
      • 5.1 左单旋
      • 5.2 右单旋
      • 5.3 双旋之左右双旋
      • 5.4 双旋之右左双旋
  • 六、 AVL树的验证
  • 七、总结

一、AVL树的概念

为了解决二叉搜索树的缺陷,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(超过1则需要对结点进行旋转),即可降低树的高度,确保整颗树的深度为O(logN)

因此,如果二叉搜索树中节点具备以下性质

  1. 它的左右子树都是AVL
  2. 右子树的高度 - 左子树的高度(简称平衡因子)的绝对值不超过1

【C++进阶】AVL树_第2张图片

那么这颗搜索二叉树就是一颗AVL树。

二、AVL树的定义

AVL树在原二叉搜索树的基础上添加了平衡因子(Balance factor),简写: bf,以及用于快速向上调整的父亲指针parent(为什么定义指针变量parent,在插入部分会介绍到),所以AVL 树是一个三叉链结构

// 以Key/Value模型为例
#include 
#include 
using namespace std;

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv; // 存储的值
	AVLTreeNode<K, V>* _left; // 左孩子
	AVLTreeNode<K, V>* _right; // 右孩子
	AVLTreeNode<K, V>* _parent; // 用于快速向上调整的父亲
	int _bf; // 平衡因子
	
	// 默认构造函数
	AVLTreeNode(const pair<K, V>& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		, _bf(0)
	{}
};

至于AVLTree类中,成员变量只需要创建一个 根节点_root即可

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;

public:
	// 默认构造
	// 初始化_root
	AVLTree()
		:_root(nullptr)
	{}

private:
	Node* _root;
};

三、如何更新平衡因子

注意: 这篇博客规定平衡因子差值为右 - 左。当然左 - 右也是可以的,根据自己的个人习惯。


【C++进阶】AVL树_第3张图片

通过上图,我们可以发现一个结论: 左边多一个结点,其祖先的路径上的平衡因子_bf--

【C++进阶】AVL树_第4张图片

同理的,右边多一个结点,其祖先的路径上的平衡因子_bf++

【总结】

  • 左边多一个结点,其祖先的路径上的平衡因子_bf--
  • 右边多一个结点,其祖先的路径上的平衡因子_bf++

四、如何控制平衡

将结点插入以后,我们需要做的就是控制平衡

因此,就有以下3种情况

  1. parent的平衡因子为0时,说明插入的结点已经把矮的那边给补上了,那么就不需要沿着祖先向上更新

【C++进阶】AVL树_第5张图片

  1. parent的平衡因子为1或者-1,就要沿着祖先的路径向上检查是否要更新。(祖先可能会不平衡)

【C++进阶】AVL树_第6张图片

  1. parent的平衡因子为2或者-2说明不平衡解决方法:对parent所在的子树进行旋转。(具体后面再谈)

根据以上分析,我们就可以写出大致的AVL插入的框架

bool insert(const pair<K, V>& key)
{
	// 如果一开始根结点为空,直接插入即可
	if (_root == NULL)
	{
		_root = new Node(key); // new会自动调用自定义类型的构造函数
		return true;
	}

	// 如果一开始根结点不为空,就要找到合适的位置插入
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key.first < key.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key.first > key.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else // 出现数据冗余,插入失败
		{
			return false;
		}
	}
	// 当cur走到空,说明已经找到了合适的位置
	cur = new Node(key);
	if (parent->_key.first < key.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	// 以上的代码基本和搜索二叉树一样
	
	// 更新成员变量_parent
	cur->_parent = parent; 

	// 控制平衡并更新平衡因子_bf(重要部分)

	// parent向上更新至多要到根结点root的_parent
	while (parent)
	{
		// 更新平衡因子
		// 左边多一个结点,父亲的平衡因子--
		if (cur == parent->_left)
		{
			parent->_bf--;
		}
		// 右边多一个结点,父亲的平衡因子++
		else  // cur == parent->_right
		{
			parent->_bf++;
		}
		
		// 控制平衡
		// parent的平衡因子为0就不需要向上控制平衡了
		if (parent->_bf == 0)
		{
			break;
		}

		// parent的平衡因子为1或者-1
		// 就要沿着祖先的路径向上检查是否有不平衡的情况
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			// 迭代
			cur = parent;
			parent = parent->_parent;
		}

		// parent的平衡因子为2或者-2,说明不平衡。
		// 解决方法:对parent所在的子树进行旋转
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
				// 旋转部分
				// ....
				
				// 旋转完后一定平衡,直接退出即可
				break;
		}
			
		// 如果断言错误出现在这一行,说明在一开始插入前,平衡因子就出现了错误
		else 
		{
			assert(false);
		}
	}

	return true;
}
解答为需要定义parent指针变量

【C++进阶】AVL树_第7张图片

因此,定义parent指针变量就是为了快速找到某结点的父亲,从而快速更新平衡因子以及快速判断是否需要进行旋转操作,减低了遍历子树来找到父结点的时间

五、插入操作(重点)

旋转需要注意的问题

  1. 还是需要保持它是一颗具有搜索树的性质(左子树比根小,右子树比根大)

  2. 让它变成平衡树,且减低这个子树的高度

5.1 左单旋

【C++进阶】AVL树_第8张图片

左单旋是为了处理当某个结点的右子树过深而导致失衡的情况(parent->_bf == 2 && cur->_bf == 1。具体步骤如下:

  1. cur的左结点作为parent的右结点
  2. 再将parent结点作为cur的左结点

当然还要考虑很多细节问题,比如:需要更新curparent的父亲以及树可能是作为子树存在等,具体看看代码实现。

【代码实现】

else if (parent->_bf == 2 || parent->_bf == -2)
{
		// 1. 左旋转(右边高)
		if (parent->_bf == 2 && cur->_bf == 1)
		{
			RotateLeft(parent);
		}
		
		break;
}

// 左旋转函数实现
void RotateLeft(Node* parent)
{
	// 记录cur和cur的左孩子
	Node* cur = parent->_right;
	Node* curleft = cur->_left;

	// ========   旋转  ==========

	// 1. 将cur的左结点,也就是curleft作为parent的右结点
	parent->_right = curleft;
	// 2. 再将parent结点作为cur的左结点
	cur->_left = parent;
	
	// =======  更新父亲关系 ======= 

	// 修改curleft的父亲,但可能存在可能不存在
	if (curleft)
	{
		curleft->_parent = parent;
	}
	
	Node* ppnode = parent->_parent;

	// 修改parent的父亲
	parent->_parent = cur;
	
	// 考虑是否为子树的情况
	// 为根的情况
	if (ppnode == nullptr)
	{
		_root = cur;
		cur->_parent = nullptr; // 根节点的父亲本身就为空
	}
	// 是子树的情况
	else
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = cur;
		}
		else
		{
			ppnode->_right = cur;
		}
		// 更新父亲
		cur->_parent = ppnode;
	}

	// 最后更新(修改)平衡因子
	parent->_bf = cur->_bf = 0;
}

5.2 右单旋

右单旋本质上和左单旋一样,有种对称的感觉~
【C++进阶】AVL树_第9张图片
右单旋是为了处理当某个节点的左子树过深而导致失衡的情况(parent->_bf == -2 && cur->_bf == -1。具体步骤如下:

  1. cur的右结点作为parent的左结点
  2. 再将parent结点作为cur的右结点

当然还要考虑很多细节问题,比如:需要更新curparent的父亲以及树可能是作为子树存在等,具体看看代码实现。

【代码实现】

else if (parent->_bf == 2 || parent->_bf == -2)
{
		// 右单旋
		else if (parent->_bf == -2 && cur->_bf == -1)
		{
			RotateRight(parent);
		}
		
		break;
}


// 右旋转函数实现
void RotateLeft(Node* parent)
{
	// 记录cur和cur的右孩子curRight
	Node* cur = parent->_left;
	Node* curRight = cur->_right;

	// ========   旋转  ==========

	// 将cur的右孩子curRight接到parent的left
	parent->_left = curRight;
	// 接着让parent替代cur右的位置
	cur->_right = parent;

	// =======  更新父亲关系 ======= 

	// 修改curRight的父亲,但可能存在可能不存在
	if (curRight)
	{
		curRight->_parent = parent;
	}

	Node* ppnode = parent->_parent;

	// 修改parent的父亲
	parent->_parent = cur;

	// 考虑是否为子树的情况
	// 为根的情况
	if (ppnode == nullptr)
	{
		_root = cur;
		cur->_parent = nullptr; // 根节点的父亲为空
	}
	// 是子树的情况
	else
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = cur;
		}
		else
		{
			ppnode->_right = cur;
		}
		// 更新父亲
		cur->_parent = ppnode;
	}

	// 最后更新(修改)平衡因子
	parent->_bf = cur->_bf = 0;
}

5.3 双旋之左右双旋

【C++进阶】AVL树_第10张图片

从上图A样例发现:parent的左子树高,因此很容易可以想到右单旋来控制平衡。但是,通过图B发现,右单旋还是解决不了问题。

那么,如果是以上这种 折线型不平衡的情况,我们可以考虑使用双旋来解决


左右双旋是为了处理parent的左子树cur的右子树过深而导致失衡的情况(parent->_bf == -2 && cur->_bf == 1。具体步骤如下:

  • 先对cur结点进行左旋操作(因为cur右子树过深)
  • 最后对parent结点进行右旋操作

我们可以根据以上步骤来验证

【C++进阶】AVL树_第11张图片

通过以上分析,有的人可能会旋转代码复用,几行代码就搞定了。

如果这样写就错了,左单旋接口和右单旋接口会默认把curparent的平衡因子置为0双旋后的curparent的平衡因子不一定为0,因此 需要考虑旋转后平衡因子的情况。

  • curRight的平衡因子为0时(没有孩子),左右双旋后parentcurcurRight平衡因子都为0
    【C++进阶】AVL树_第12张图片

  • curRight的平衡因子为-1时(有左孩子),左右双旋后parentcurcurRight平衡因子分别为100

【C++进阶】AVL树_第13张图片

  • curRight的平衡因子为1时(有右孩子),左右双旋后parentcurcurRight平衡因子分别为0-10

    【C++进阶】AVL树_第14张图片

【代码实现】

else if (parent->_bf == 2 || parent->_bf == -2)
{
		// 左右双旋
		else if (parent->_bf == -2 && cur->_bf == 1)
		{
			RotateLR(parent);
		}

		break;
}


void RotateLR(Node* parent)
{
	Node* cur = parent->_left;
	Node* curRight = cur->_right;
	int curRight_of_bf = curRight->_bf;

	RotateLeft(cur);
	RotateRight(parent);

	if (curRight_of_bf == 0)
	{
		parent->_bf = 0;
		cur->_bf = 0;
		curRight->_bf = 0;
	}
	else if (curRight_of_bf == -1)
	{
		parent->_bf = 1;
		cur->_bf = 0;
		curRight->_bf = 0;
	}
	else if (curRight_of_bf == 1)
	{
		parent->_bf = 0;
		cur->_bf = -1;
		curRight->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

5.4 双旋之右左双旋

【C++进阶】AVL树_第15张图片

右左双旋是为了处理parent结点的右结点cur的左子树过深而导致失衡的情况(parent->_bf == 2 && cur->_bf == -1。具体步骤如下:

  1. 先对cur结点进行右旋操作(cur左子树过深)
  2. 最后再对parent结点进行左旋操作

右左旋和左右旋类似,手撕代码之前同样需要考虑旋转后平衡因子的情况

  • curLeft的平衡因子为0时(没有孩子),右左双旋后parentcurcurLeft平衡因子都为0

【C++进阶】AVL树_第16张图片

  • curLeft的平衡因子为-1时(有左孩子),右左双旋后parentcurcurLeft平衡因子分别为都为010

【C++进阶】AVL树_第17张图片

  • curLeft的平衡因子为1时(有右孩子),右左双旋后parentcurcurLeft平衡因子分别为都为-100

【C++进阶】AVL树_第18张图片

【代码实现】

else if (parent->_bf == 2 || parent->_bf == -2)
{
		// 右左双旋
		else if (parent->_bf == 2 && cur->_bf == -1)
		{
			RotateRL(parent);
		}

		break;
}

void RotateRL(Node* parent)
{
	Node* cur = parent->_right;
	Node* curLeft = cur->_left;
	int curLeft_of_bf = curLeft->_bf;

	RotateRight(cur);
	RotateLeft(parent);

	if (curLeft_of_bf == 0)
	{
		cur->_bf = 0;
		curLeft->_bf = 0;
		parent->_bf = 0;
	}
	else if (curLeft_of_bf == 1)
	{
		cur->_bf = 0;
		curLeft->_bf = 0;
		parent->_bf = -1;
	}
	else if (curLeft_of_bf == -1)
	{
		cur->_bf = 1;
		curLeft->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

六、 AVL树的验证

通过平衡因子检查检查即可。平衡因子反映的是左右子树高度之差(本篇博客是:右子树 - 左子树)。通过计算出左右子树高度之差并与当前节点的平衡因子进行比对,如果发现不同,则说明 AVL 树非法。

注意:如果当前节点的 平衡因子 取值范围不在[-1, 1]内,也可以判断非法

// 验证AVL树
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()
{
	return IsBalance(_root);
}

bool IsBalance(Node* root)
{
	if (root == nullptr)
	{
		return true;
	}
	int leftHeight = Height(root->_left);
	int rightHeight = Height(root->_right);

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

	return abs(rightHeight - leftHeight) < 2
		&& IsBalance(root->_left)
		&& IsBalance(root->_right);
}

通过一段简单的代码,随机插入1w个节点,判断是否合法

#include "AVL.h"
#include 
int main()
{
	const int N = 10000;
	vector<int> v(N);
	srand((size_t)time(NULL));

	for (int i = 0; i < N; i++)
	{
		v.push_back(rand());
	}

	AVLTree<int, int> t;
	for (auto x : v)
	{
		t.insert(make_pair(x, x));
		cout << "Insert:" << x << "->" << t.IsBalance() << endl;
	}
	return 0;
}

当打印出来的结果全为1(表示真),那么它就是一个AVL

七、总结

AVL 树是一棵 绝对平衡 的二叉树,对高度的控制极为苛刻,稍微有点退化的趋势,都要被旋转调整,这样做的好处是 严格控制了查询的时间,查询速度极快,时间复杂度为 logN。而对于删除,大家不用担心,因为在面试的时候只会考察AVL树的插入操作hh

这是本篇博客的相关代码:代码仓库。

你可能感兴趣的:(C++,c++,java,开发语言,自动化,linux,运维,服务器)