图解AVL树:从平衡原理到四种旋转的C++实现

188 阅读9分钟

1. AVL树的概念

  • AVL树是最先发明的自平衡二叉搜索树,是一颗高度平衡的搜索二叉树,通过控制高度差来控制平衡,它可以是一棵空树或者具备以下性质的二叉搜索树:

    • 左右子树都是AVL树,且左右子树都是AVL树。
    • 左右子树的高度差绝对值不超过1。
  • AVL树的名字来源于其两位发明者的姓氏首字母,他们分别是苏联数学家G. M. Adelson-Velsky(Г. М. Адельсон-Вельский)和E. M. Landis(Е. М. Ландис)。两人在1962年发表的论文《An algorithm for the organization of information》中首次提出了这种数据结构,因此以他们姓氏的首字母组合命名为AVL树

  • AVL树中引入了一个平衡因子(balance factor) 的概念,每个节点都有一个平衡因子,任何节点的平衡因子等于右子树高度减左子树的高度(在其他地方也可能是左减右,效果是一样的),也就是任何节点的平衡因子==0/1/-1,AVL树不是必须要平衡因子,但是有了平衡因子可以更方便我们去观察和控制。

  • 为什么高度差不超过1,而不是0?因为在有些情况下是做不到高度差为0的,比如一棵树只有2个节点或者4个节点的时候,就无法满足高度差为0,所以高度差最好就是1。

  • AVL树节点分布和完全二叉树类似,高度可以控制在logNlogN,增删改查的效率也可以控制在O(logN)O(logN),相比普通的二叉搜索树有了提升。

2. AVL树的结构

在下面节点的结构代码中有一个 _parent 指针,这是为了方便后面进行平衡因子调整时进行回溯,但也会有相应的代价(需要多维护一个指针),在其他地方实现时也可能会使用其他方式,例如使用栈来存储路径,总体思想大同小异。

template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;    // 这里实现 K/V 场景,pair 的使用参考 map 容器
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf;	// 平衡因子(balance factor)
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)  
	{}
};

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
private:
	Node* _root = nullptr;
};

3. AVL树插入与平衡因子更新

3.1 AVL树插入

  1. 按照二叉搜索树的规则进行插入。
  2. 插入节点后,会影响该路径高度,也就是会影响部分祖先结点的平衡因子,所以更新从新增节点->根节点路径上的平衡因子(最差情况),有些情况只需更新到中间就停止,下面再进行分析。
  3. 更新平衡因子过程中没有出现问题,则插入结束。
  4. 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后调平衡同时,本质上降低了子树的高度,不会影响上一层,所以插入结束。

3.2 平衡因子更新

更新原则:

  • 平衡因子 = 右子树高度 - 左子树高度(反过来也可以)。
  • 只有子树高度发生变化才会影响当前节点平衡因子。
  • 节点插入在左子树parent平衡因子--插入在右子树parent平衡因子++
  • parent所在子树的高度是否变化决定是否继续向上更新。

更新停止条件:

  • 更新后parent的平衡因子 == 0,代表更新前parent的平衡因子为 -1或1(子树一边高一边低),并且新增的节点插入在低的一边,所以插入后parent所在的子树高度不变,不会影响parent->parent(祖先节点) 节点平衡因子,所以平衡因子更新结束。
  • 更新parent的平衡因子 == 1或-1,代表更新前parent的平衡因子为0(子树两边高度相等),新插入节点后就变成了一边高一边低,虽然parent所在的子树符合要求,但是高度发生了变化,可能影响到parent->parent(祖先节点) 节点平衡因子,所以要继续向上更新。
  • 更新parent的平衡因子 == 2或-2,代表更新前parent的平衡因子为 -1或1(子树一边高一边低),并且新增的节点插入在了本来就高的那一边,导致不平衡,所以需要进行旋转处理。旋转后会把parent子树变平衡,并且降低高度,所以旋转后也不需要继续向上更新。
  • 不断更新,最差情况更新到根节点。

例如下图,未插入之前,10节点平衡因子为1,然后新节点插入在本身就高的那一边,导致10节点平衡因子增加到2,所以需要更新平衡因子,一直更新到10节点。

image.png

4. 旋转

  1. 保持搜索树的规则。
  2. 让被旋转的树变平衡,降低被旋转树的高度。

共有四种旋转:左单旋、右单旋、左右双旋、右左双旋。

4.1 右单旋

在下图1中表达为:有a、b、c三棵抽象为高度为h的子树,且均符合AVL树的要求,节点10可以认为是根节点,或者也可以认为是某一棵子树的根。

  • 在a子树中插入一个新节点,a子树的高度变为h+1,向上更新平衡因子,导致10的平衡因子从-1变为-2,10为根节点的这棵树左右高度差超过1,不满足平衡规则。并且因为左边高,所以需要向右旋转,从而控制平衡。
  • 因为根据二叉搜索树的规则,比当前节点小的值向左走,比当前节点大的值向右走,所以5 < b子树的值 < 10,将b子树变为10的左子树,10变为5的右子树,5变成当前这棵树的根节点,符合搜索树的规则。并且控制了平衡,同时将树的高度恢复到了插入之前的h+2,符合旋转的原则。如果插入之前10节点这棵树为一个局部子树,旋转后也不会影响上一层,所以插入结束。

image.png

图1

image.png

图2

image.png

图3

4.2 右单旋代码实现

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

	parent->_left = subLR;
	subL->_right = parent;

	if (subLR) { subLR->_parent = parent; }
	Node* parentParent = parent->_parent;
	parent->_parent = subL;

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

4.3 左单旋

在下图4中表达为:有a、b、c三棵抽象为高度为h的子树,且均符合AVL树的要求,节点10可以认为是根节点,或者也可以认为是某一棵子树的根。逻辑与右单旋相反。

  • 在a子树中插入一个新节点,a子树的高度变为h+1,向上更新平衡因子,导致10的平衡因子从1变为2,10为根节点的这棵树左右高度差超过1,不满足平衡规则。并且因为右边高,所以需要向左旋转,从而控制平衡。
  • 因为10 < b子树的值 < 15,将b子树变为10的右子树,10变为15的左子树,15变成当前这棵树的根节点,符合搜索树的规则。并且控制了平衡,同时将树的高度恢复到了插入之前的h+2,符合旋转的原则。如果插入之前10节点这棵树为一个局部子树,旋转后也不会影响上一层,所以插入结束。

image.png

图4

4.4 左单旋代码实现

// 左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	subR->_left = parent;

	if (subRL) { subRL->_parent = parent; }
	Node* parentParent = parent->_parent;
	parent->_parent = subR;

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

4.5 左右双旋

通过下面图5图6可以看到,左边高的时候,如果插入位置不是在a子树,而是在b子树,导致b子树高度从h变为h+1,从而引发旋转,右单旋无法解决问题,右单旋后树依然不平衡。右单旋解决的是纯粹的左边高,但是在插入b子树后,10为根的子树不再是单纯的左边高,对于10是左边高,但是对于5是右边高,所以需要用两次旋转解决,以5为旋转点进行一个左单旋,以10为旋转点进行一个右单旋,这棵树就平衡了。

image.png

图5

image.png

图6

图5和图6分别为h==0和h==1,下面将a、b、c子树抽象为高度h的AVL树进行分析,另外把b子树进一步展开为节点8和高度h-1的e和f子树,因为要对b的父节点5为旋转点进行左单旋,左单旋需要动b树中的左子树。b子树新增节点的位置不同,平衡因子更新的细节也不同,通过观察节点8的平衡因子,要分三个场景讨论。

  1. h>=1时,新增节点插入在e子树,e子树高度从h-1变为h并不断更新8->5->10的平衡因子,引发旋转,其中8的平衡因子为-1,旋转后8和5平衡因子为0,10平衡因子为1。
  2. h>=1时,新增节点插入在f子树,f子树高度从h-1变为h并不断更新8->5->10的平衡因子,引发旋转,其中8的平衡因子为-1,旋转后8和5平衡因子为0,10平衡因子为-1。
  3. h==0时,a、b、c都是空树,b自己就是一个新增节点,不断更新5->10的平衡因子,引发旋转,其中8的平衡因子为0,旋转后8、10、5平衡因子均为0。

image.png

图7

4.6 左右双旋代码实现

// 左右双旋
// 复用左单旋和右单旋函数
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;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else { assert(false); }
}

4.6 右左双旋

逻辑与左右双旋相同,这里不再做演示。

4.7 右左双旋代码实现

// 右左双旋
// 复用左单旋和右单旋函数
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;
		subR->_bf = 1;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		parent->_bf = -1;
	}
	else if (bf == 0)
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		parent->_bf = 0;
	}
	else { assert(false); }
}

5. AVL树的查找与删除

  1. 查找:按照二叉搜索树逻辑实现即可,可以看之前的文章。
  2. 删除:可参考《数据结构:⽤⾯向对象⽅法与C++语⾔描述》。