一.AVL树的概念

二叉搜索树最然可以缩短查找效率,但如果数据有序或接近有序的话,二叉搜索树就会退化为单支树,查找元素相当于在顺序表中搜索元素一样,效率低下。因此,两位俄罗斯的数学家 G.M. Adelson-Velsky 和 E.M. Landis 发明的一种解决上述问题的方法:当二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,就需要对树中的结点进行调整,就是降低树的高度,从而减少平均搜索长度。

AVL树的特征:

  • 整棵树的每个结点的左右分支都是一颗AVL树
  • 整棵树的所有结点的左右子树的高度之差(平衡因子balance factor)的绝对值不超过1(-1,0,1)

当一颗二叉搜索树高度平衡时,他就是AVL树,如果共有n个结点,它的高度就可以保持在log2N,搜索的时间复杂度就是O(log2N)。

二.AVL树的定义:

template<class K>
struct AVLTreeNode
{
	AVLTreeNode<K>* _parent;
	AVLTreeNode<K>* _left;
	AVLTreeNode<K>* _right;
	K _key;
	int _bf;
	AVLTreeNode(const K& key)
		:_parent(nullptr)
		,_left(nullptr)
		,_right(nullptr)
		,_key(key)
		,_bf(0)
	{}
};

之所以使用三叉链结构,是因为

  1. AVL树的核心是通过平衡因子(BF)调整树的平衡性。当插入或删除节点时,需要沿着路径向上回溯到根节点,逐层更新祖先节点的平衡因子。通过_parent指针可以直接访问父节点,代码更简洁高效
  2. AVL树的旋转(左旋、右旋、双旋)需要调整父子节点的指向关系,涉及多个节点的指针修改。通过_parent指针,可以直接修改父节点对子节点的引用,无需从根节点重新定位父节点,减少指针操作的复杂度。

三.AVL树的插入

AVL树就是在二叉搜索树得基础上引入平衡因子,因此AVL树也可以看成二叉搜索树。因此,AVL树的插入过程可以分为两步:

  1. 按照二叉搜索树的方式插入新结点
  2. 插入结点的同时,调节平衡因子,保证平衡因子的绝对值小于2(也就是左右子树的高度差不超过1)

当一个新节点插入时,AVL树可能会遭到破坏,此时就需要更新平衡因子,之后监测平衡因子的值,判断新插入的结点是否破坏了AVL树的平衡性,从而选择是否需要旋转。

  1. 如果更新完以后,平衡没有出现问题(|bf| <  2),平衡结构没有影响,不需要处理,继续插入即可
  2. 如果更新完后,平衡出现问题(|bf| >= 2),平衡受到影响,需要处理(旋转)

插入新的结点,会影响祖先的平衡因子

			if (parent->_right == cur)
			{
				parent->_bf++;
			}
			else if(parent->_left == cur)
			{
				parent->_bf--;
			}

更新完结点后,是否需要继续往上更新,取决于parent所在的子树的高度变化,变了继续更新,不变则不再更新。

  • parent->_bf == 1 || parent->_bf == -1 更新结束后父结点是这样的值,证明在更新前左右子树的高度相同,现在新增一个结点,说明高度一边高一边低,这时就需要向上更新。

  • parent->_bf == 0 更新结束后父结点是这样的值,证明更新前左右子树一边高一边低,这时插入的结点弥补了低的那一边,这样树的高度没有发生变化,这样就不需要往上继续更新。

  • parent->_bf == 2 || parent->_bf == -2 更新结束后父结点是这样的值,就需要对这棵子树进行旋转处理
	while (parent)
	{
		//新增结点后,对父结点的平衡因子进行更新。
		if (parent->_right == cur)
		{
			parent->_bf++;
		}
		else if (parent->_left == cur)
		{
			parent->_bf--;
		}

		if (parent->_bf == 1 || parent->_bf == -1)
		{
			//继续往上更新
			parent = parent->_parent;
			cur = cur->_parent;
		}
		else if (parent->_bf == 0)
		{
			break;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			//旋转处理
		}
		else
		{
			assert(false);
		}
	}

AVL树的旋转

如果在一棵树原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据结点插入的位置不同,AVL树的旋转分为四种:(红色方块代表新增结点,h代表子树的高度

  • 新结点插入较高的右子树的右侧:进行左单旋

将对于一颗树所有左单旋的情况用抽象图(树中的子树除外)表示为:

当子树高度为0时(h = 0)

当子树高度为1时(h = 1)

当子树的高度为2时(h = 2)

对于h这颗子树来讲,h=2的时候总共有三种情况

这样根据简单运算一下,总共可能的情况就会有3*3*3 = 27种,但是这里是需要左旋,这就表示c必须是图3这种情况,而a和b可以是上面三种图像中的任意一种。所以总共有3*3*1=9种,

这时候就有人好奇了c为什么就不能是图1和图2呢,我们来试一试就知道了:

用上图2就会出现这两种情况,图a的情况不会发生旋转,平衡因子都小于2,而图b明显可以看到这种左旋就成为了这颗树的子树的左旋,而我们现在讨论的是这一整棵树的左旋。所以要进行整棵树的左旋,c必须是上面图3那种情况,否则就是不旋转或者就是进入到子树进行旋转。

因此,只要新增的结点是4个红色结点的其中之一,就可以对整棵树进行左旋。举其中一例:

可以明显看到,插入后新结点69导致以30为根的二叉树不平衡了,要想让30平衡,只能将30的右子树的高度减少一层,左子树增加一层,就是将右子树往上提,这样30就转下来了,因为30比60小,只能放在60的左子树,如果60有左子树,左子树的根一定大于30,小于60,只能放在30的右子树,旋转完成之后,更新平衡因子。

在旋转的过程中,还需要考虑:

  1. 60的左子树可能存在,也可能不存在(存在需要进行维护它的结点指针,比如旋转后更新60左子树的父结点)

  2. 30可能是根节点,也可能是子树,如果是根节点,旋转完成之后,就要更新根节点;如果是子树,则可能是某一个结点的左子树,也有可能是右子树。,就需要将其与祖先相连

左旋的实现:

void RatateL(Node* parent)
{
    //subR是parent的右孩子
	Node* subR = parent->_right;
    //subRL是subR的左孩子
	Node* subRL = subR->_left;
    
    //旋转完成之后,60的左孩子做30的右孩子
	parent->_right = subRL;
    
    //如果60的左孩子存在,就要更新它的父节点,让它的父结点指向30
	if (subRL)
	{
		subRL->_parent = parent;
	}
    //记录30的父结点,因为30也可能是子树
	Node* ppnode = parent->_parent;
    
    //将30连接到60的左子树        
	subR->_left = parent;
    //更新30的父结点
	parent->_parent = subR;

	//如果30是根节点,将60作为新的根节点
	if (ppnode == nullptr)
	{
		subR->_parent = nullptr;
		_root = subR;
	}
	else
	{
        //如果30是子树,可能是左子树,也可能是右子树
		if (ppnode->_left == parent)
		{
			ppnode->_left = subR;
		}
		else
		{
			ppnode->_right = subR;
		}
		subR->_parent = ppnode;
	}

    //更新平衡因子
	parent->_bf = subR->_bf = 0;
}

  • 新节点插入较高的左子树的左侧:进行右单旋           ​​​​

当h = 0时

当h = 1时

当h = 2时

同样a位置只能是图3,而b和c可以是图中任意一种,这样就满足了右旋的特征,所有可能的情况有:

为什么a只能是图3,和左旋一样

要么就是不旋转,要么就是成为了分支的右旋

上面所有的情况和这幅图一样,只要是插入的是图中4个结点中的一个,都会造成右旋,举例:

在旋转的过程中,同样需要考虑30的右子树存在与否

还有就是60可能是根节点,也可能是左子树,也可能是右子树

右旋的实现:

void RatateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	if (subLR)
	{
		subLR->_parent = parent;
	}

	Node* ppnode = parent->_parent;
	subL->_right = parent;
	parent->_parent = subL;

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

	parent->_bf = subL->_bf = 0;
}
  • 新节点插入较高左子树的右侧:先进行左旋,在进行右旋   

当h>=1时

将双旋变成单旋后再旋转,就是先对30进行左单旋,然后再对90进行右单旋,旋转完成之后再考虑平衡因子的更新

当h<1时

当h = 1时

当h = 2时 所有可能的情况:

同样,上面所有情况和下面这幅图一样的4个红色节点位置,只要在这4个结点其中之一插入,就可以触发左右旋转。

实例:

到这,就有好奇的人问了,右旋的parent的平衡因子和这里左右旋的平衡因子都是-2,我要是偏偏就想直接右旋,会发生什么?

现在试试左右选直接右旋会发生什么?

    可以看到直接右旋的话,依旧没有效果,平衡因子大于1,所以不可以直接右旋,必须先对30左旋,再对90右旋,这样才能达到平衡的。

    左右旋的实现

    void RatateLR(Node* parent)
    {
    	Node* subL = parent->_left;
    	Node* subLR = subL->_right;
    	RatateL(subL);
    	RatateR(parent);
    
    	if (subLR->_bf == 1)
    	{
    		parent->_bf = 0;
    		subL->_bf = -1;
    		subLR->_bf = 0;
    	}
    	else if (subLR->_bf == -1)
    	{
    		parent->_bf = 1;
    		subL->_bf = 0;
    		subLR->_bf = 0;
    	}
    	else if (subLR->_bf == 0)
    	{
    		parent->_bf = subL->_bf = subLR->_bf = 0;
    	}
    	else
    	{
    		assert(false);
    	}
    }

     左右旋的实现并不难,只要会了左旋和右旋,左右旋就可以了,左右旋唯一的难点是平衡因子的控制,更新完成之后如何控制平衡因子,是一大问题?------

    仔细观察图中可以发现我们可以通过判断subL的右子树(记为subLR)的平衡因子进行判断,用通俗的话来讲左右旋就是,将subLR的左孩子给了subL,将subLR的右孩子给了parent,然后subLR做subL和parent的父结点,因此:

    1. 当subLR的平衡因子为0时,证明subLR是新增结点,可以明显看到,旋转之后parent,subL,subLR三者的平衡因子都是0。
    2. 当subLR的平衡因子为1时,证明新增结点在subLR的右边, 这样给了parent,parent就平衡了,而subL就会左边高,他的平衡因子就是-1,parent和subLR就平衡了,平衡因子为0.
    3. 当subLR的平衡因子为-1时,证明新增结点在subLR的左边, 这样给了subL,subL就平衡了,而parent就会右边高,他的平衡因子就是1,subL和subLR就平衡了,平衡因子为0.
    • 新结点插入右子树的左侧:先进行右旋,在进行左旋

    当h>=1时

    也是将双旋变为单旋后在旋转,就是对90进行右旋,再对30进行左旋,旋转完成之后再进行平衡因子的更新。

    当h < 0时

    当h = 1时 

    当h = 2 时,所有可能的情况:

     

    同样,上面所有情况与下面这幅图相同,只要插入这4个红色结点的其中之一 ,就会造成右左旋

    实例:

    如果直接左旋就会出现:

    依旧无法解决问题,所以只能先对60进行右旋,在对30进行左旋,这样才能达到平衡。 

    右左旋的实现

    void RatateRL(Node* parent)
    {
    	Node* subR = parent->_right;
    	Node* subRL = subR->_left;
    	RatateR(subR);
    	RatateL(parent);
    
    	if (subRL->_bf == 1)
    	{
    		parent->_bf = -1;
    		subR->_bf = 0;
    		subRL->_bf = 0;
    	}
    	else if (subRL->_bf == -1)
    	{
    		parent->_bf = 0;
    		subR->_bf = 1;
    		subRL->_bf = 0;
    	}
    	else if (subRL->_bf == 0)
    	{
    		parent->_bf = subR->_bf = subRL->_bf = 0;
    	}
    	else
    	{
    		assert(false);
    	}
    }

    平衡因子的判断

    仔细观察图中可以发现我们可以通过判断subR的左子树(记为subRL)的平衡因子进行判断,用通俗的话来讲左右旋就是,将subRL的右孩子给了subR,将subRL的左孩子给了parent,然后subRL做subR和parent的父结点,因此:

    • 当subRL的平衡因子为0时,证明subRL是新增结点,可以明显看到,旋转之后parent,subR,subRL三者的平衡因子都是0。
    • 当subRL的平衡因子为-1时,证明新增结点在subRL的左边, 这样给了parent,parent就平衡了,而subR就会右边高,他的平衡因子就是1,parent和subRL就平衡了,平衡因子为0.
    • 当subRL的平衡因子为1时,证明新增结点在subRL的右边, 这样给了subR,subR就平衡了,而parent就会左边高,他的平衡因子就是-1,subR和subRL就平衡了,平衡因子为0.

     总结所有的情况

    这就是4种旋转所有的情况,上面介绍的右左旋转和左右旋转,是为了让大家看清楚是如何旋转的,属于微观角度,现在这幅图就属于宏观情况了,所有新增结点(红色方块)上方的h=2时,都必须是图3那种情况,不然就会成为子树的旋转问题或者不旋转,这就是针对一颗树所有的旋转方式。接下来进行验证我们的成果的时刻。

    四.AVL树的验证

    像我第一次学习AVL树时,看到有序就觉得大功告成了,AVL树实现成功了,但是转念一想,真的是这样吗?不对呀,二叉搜索树中序遍历一下也是有序的,所以,我得进行进一步的验证。

    AVL树是在二叉搜索树的基础上加入了平衡性的限制,所以单单进行中序遍历等到一个有序的序列,只能证明它时二叉搜索树,不能证明他是AVL树,所以就要进一步进行验证:每一个平衡因子的绝对值不能超过1,这样才能算AVL树。所以通过实现一个Is_balance函数,进行对每一个结点进行检测,看看是否左右子树的高度差为1.

    	int _Height(Node* root)
    	{
    		if (root == nullptr)
    		{
    			return 0;
    		}
    		int left = _Height(root->_left);
    		int right = _Height(root->_right);
    		return left > right ? left + 1 : right + 1;
    	}
    	bool Is_balance(Node* root)
    	{
    		if (root == nullptr)
    		{
    			return true;
    		}
    		int leftH = _Height(root->_left);
    		int rightH = _Height(root->_right);
    
    		return abs(leftH - rightH) < 2 && Is_balance(root->_left) && Is_balance(root->_right);
    	}

    五.AVL树的性能

    AVL树是一颗绝对平衡的二叉搜索树,其每一个节点的左右子树的高度差的绝对值不超过1,这样就能保证时间复杂度为O(log2N),但是插入时需要维护绝对平衡,旋转的次数也比较多,所以绝对平衡虽然查询很高效,但是在插入的时候也就比较痛苦了,这样插入的效率就比较低了。因此,如果需要一种查询高效,有序且是不轻易改变数据的数据结构时,AVL树就可以首选。

    AVL树的完整实现代码:

    #include<iostream>
    #include<assert.h>
    using namespace std;
    
    template<class K>
    struct AVLTreeNode
    {
    	AVLTreeNode<K>* _parent;
    	AVLTreeNode<K>* _left;
    	AVLTreeNode<K>* _right;
    	K _key;
    	int _bf;
    	AVLTreeNode(const K& key)
    		:_parent(nullptr)
    		,_left(nullptr)
    		,_right(nullptr)
    		,_key(key)
    		,_bf(0)
    	{}
    };
    
    template<class K>
    class AVLTree
    {
    	typedef AVLTreeNode<K> Node;
    public:
    	bool Insert(const K& key)
    	{
    		if (_root == nullptr)
    		{
    			_root = new Node(key);
    			return true;
    		}
    
    		Node* parent = nullptr;
    		Node* cur = _root;
    
    		while (cur)
    		{
    			if (cur->_key > key)
    			{
    				parent = cur;
    				cur = cur->_left;
    			}
    			else if (cur->_key < key)
    			{
    				parent = cur;
    				cur = cur->_right;
    			}
    			else
    			{
    				return false;
    			}
    		}
    
    		cur = new Node(key);
    		if (parent->_key > key)
    		{
    			parent->_left = cur;		
    		}
    		else
    		{
    			parent->_right = cur;	
    		}
    		cur->_parent = parent;
    		while (parent)
    		{
    			if (parent->_right == cur)
    			{
    				parent->_bf++;
    			}
    			else if(parent->_left == cur)
    			{
    				parent->_bf--;
    			}
    
    			if (parent->_bf == 1 || parent->_bf == -1)
    			{
    				parent = parent->_parent;
    				cur = cur->_parent;
    			}
    			else if (parent->_bf == 0)
    			{
    				break;
    			}
    			else if (parent->_bf == 2 || parent->_bf == -2)
    			{
    				if (parent->_bf == 2 && cur->_bf == 1)
    				{
    					RatateL(parent);
    				}
    				else if (parent->_bf == -2 && cur->_bf == -1)
    				{
    					RatateR(parent);
    				}
    				else if (parent->_bf == 2 && cur->_bf == -1)
    				{
    					RatateRL(parent);
    				}
    				else if (parent->_bf == -2 && cur->_bf == 1)
    				{
    					RatateLR(parent);
    				}
    				break;
    			}
    			else
    			{
    				assert(false);
    			}
    		}
    		return true;
    	}
    
    	void Inorder()
    	{
    		_Inorder(_root);
    		cout << endl;
    	}
    
    	bool IsBlance()
    	{
    		return Is_balance(_root);
    	}
    
    private:
    	void RatateL(Node* parent)
    	{
    		Node* subR = parent->_right;
    		Node* subRL = subR->_left;
    		parent->_right = subRL;
    		if (subRL)
    		{
    			subRL->_parent = parent;
    		}
    
    		Node* ppnode = parent->_parent;
    
    		subR->_left = parent;
    		parent->_parent = subR;
    
    		
    		if (ppnode == nullptr)
    		{
    			subR->_parent = nullptr;
    			_root = subR;
    		}
    		else
    		{
    			if (ppnode->_left == parent)
    			{
    				ppnode->_left = subR;
    			}
    			else
    			{
    				ppnode->_right = subR;
    			}
    			subR->_parent = ppnode;
    		}
    
    
    		parent->_bf = subR->_bf = 0;
    	}
    
    	void RatateR(Node* parent)
    	{
    		Node* subL = parent->_left;
    		Node* subLR = subL->_right;
    
    		parent->_left = subLR;
    		if (subLR)
    		{
    			subLR->_parent = parent;
    		}
    
    		Node* ppnode = parent->_parent;
    		subL->_right = parent;
    		parent->_parent = subL;
    
    		if (ppnode == nullptr)
    		{
    			subL->_parent = nullptr;
    			_root = subL;
    		}
    		else
    		{
    			if (ppnode->_left == parent)
    			{
    				ppnode->_left = subL;
    			}
    			else
    			{
    				ppnode->_right = subL;
    			}
    			subL->_parent = ppnode;
    		}
    
    		parent->_bf = subL->_bf = 0;
    	}
    
    	void RatateRL(Node* parent)
    	{
    		Node* subR = parent->_right;
    		Node* subRL = subR->_left;
    		RatateR(subR);
    		RatateL(parent);
    
    		if (subRL->_bf == 1)
    		{
    			parent->_bf = -1;
    			subR->_bf = 0;
    			subRL->_bf = 0;
    		}
    		else if (subRL->_bf == -1)
    		{
    			parent->_bf = 0;
    			subR->_bf = 1;
    			subRL->_bf = 0;
    		}
    		else if (subRL->_bf == 0)
    		{
    			parent->_bf = subR->_bf = subRL->_bf = 0;
    		}
    		else
    		{
    			assert(false);
    		}
    	}
    	void RatateLR(Node* parent)
    	{
    		Node* subL = parent->_left;
    		Node* subLR = subL->_right;
    		RatateL(subL);
    		RatateR(parent);
    
    		if (subLR->_bf == 1)
    		{
    			parent->_bf = 0;
    			subL->_bf = -1;
    			subLR->_bf = 0;
    		}
    		else if (subLR->_bf == -1)
    		{
    			parent->_bf = 1;
    			subL->_bf = 0;
    			subLR->_bf = 0;
    		}
    		else if (subLR->_bf == 0)
    		{
    			parent->_bf = subL->_bf = subLR->_bf = 0;
    		}
    		else
    		{
    			assert(false);
    		}
    	}
    
    	void _Inorder(Node* root)
    	{
    		if (root == nullptr)
    		{
    			return;
    		}
    
    		_Inorder(root->_left);
    		cout << root->_key << " ";
    		_Inorder(root->_right);
    	}
    	int _Height(Node* root)
    	{
    		if (root == nullptr)
    		{
    			return 0;
    		}
    		int left = _Height(root->_left);
    		int right = _Height(root->_right);
    		return left > right ? left + 1 : right + 1;
    	}
    	bool Is_balance(Node* root)
    	{
    		if (root == nullptr)
    		{
    			return true;
    		}
    		int leftH = _Height(root->_left);
    		int rightH = _Height(root->_right);
    
    		return abs(leftH - rightH) < 2 && Is_balance(root->_left) && Is_balance(root->_right);
    	}
    
    
    private:
    	Node* _root = nullptr;
    };

    Logo

    魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

    更多推荐