【C++】AVL树底层思想 and 大厂面试

发布于:2025-07-09 ⋅ 阅读:(25) ⋅ 点赞:(0)

一、AVL树的概念

        AVL树是在二叉搜索树的基础上加上一些规则来控制它的高度差进而控制的它的时间复杂度。

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

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

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

注意:平衡因子不是必须的,是AVL树的特有的的,红黑树就没有,这是一种控制搜索二叉树的高度的方式。

        思考一下为什么AVL树是高度平衡搜索二叉树,要求高度差不超过1,而不是高度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是0的。比如一棵树是2个结点,4个结点等情况下,高度差最好就是1,无法做到高度差是0。

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

二、AVL树的插入

        插入一个值按二叉搜索树规则进行插入。

        新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点->根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了,具体情况我们下面再详细分析。

        更新平衡因子过程中没有出现问题,则插入结束。

        更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。

注意:更新平衡因子只会影响插入结点的祖先,所以就更新祖先即可。

三、平衡因子更新

平衡因子更新规则

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

        只有子树高度变化才会影响当前结点平衡因子。

        插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在parent的左子树,parent平衡因子--

        parent所在子树的高度是否变化决定了是否会继续往上更新。

注意:如果插入一个值导致 parent 的平衡因子变成1或者 -1,这说明 parent 原来是平衡的但是现在不平衡了,所以除了parent的平衡因子为0不用向上更新平衡因子,其他的最坏要向上更新平衡因子一直更新到根节点。 如果插入的值在右子树则只需要更新右子树的平衡因子,它不影响左子树的平衡因子;插入到左子树就只更新左子树的平衡因子。

图片示例:

当平衡因子往上更新的时,平衡因子为2/-2,则要发生旋转来控制树的平衡进而控制平衡因子。旋转之后就不用往上更新平衡因子了,因为旋转的目的是控制树的平衡。

四、旋转

 1)规则:

1、保持搜索树的规则

2、让旋转的树从不满足变平衡,其次降低旋转树的高度旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。

2)右单旋

就是左边太高了

        下图展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里a/b/c是高度为h的子树,是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体图2/图3/图4/图5进行了详细描述。

        在a子树中插入一个新结点,导致a子树的高度从h变成h+1,不断向上更新平衡因子,导致10的平衡因子从-1变成-2,10为根的树左右高度差超过1,违反平衡规则。10为根的树左边太高了,需要往右边旋转,控制两棵树的平衡。

        旋转核心步骤,因为5<b子树的值<10,将b变成10的左子树,10变成5的右子树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。

注意:a,b,c是概念子树,就是这个子树包含无数种情况。总之这个概念子树由于种种原因(插入值的不同位置)导致祖先出现了平衡因子为2或者-2。

代码示例:

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

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

		Node* ppnode = parent;

		subL->_right = parent;
		parent->_parent = subL;

		if (parent == _root)
		{
			_root = subL;
		}
		else//parent不是根节点
		{
			if (ppnode->_left == parent)//更新父亲的父亲结点的指向
			{
				ppnode->_left = subL;
			}
			else
			{
				ppnode->_right = subL;
			}
		}
		parent->_bf = subL->_bf = 0;

	}

 3)左单旋

        下图展示的是10为根的树,有a/b/c抽象为三棵高度为h的子树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里a/b/c是高度为h的子树,是一种概括抽象表示,他代表了所有右单旋的场景,实际右单旋形态有很多种,具体跟上面左旋类似。

        在a子树中插入一个新结点,导致a子树的高度从h变成h+1,不断向上更新平衡因子,导致10的平衡因子从1变成2,10为根的树左右高度差超过1,违反平衡规则。10为根的树右边太高了,需要往左边旋转,控制两棵树的平衡。

        旋转核心步骤,因为10<b子树的值<15,将b变成10的右子树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。

代码示例:

void RotateL(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 (parent == _root)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = subR;
		}
		else
		{
			ppnode->_right = subR;
		}
		subR->_parent = ppnode;
	}
	parent->_bf = subR->_bf = 0;
}

4)左右双旋

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

注意:左右双旋是针对在左子树的右子树插入结点的情况进行分析。

总结:左单旋、右单旋是由整体的树的插入最左结点或者最右结点引发的,其他情况就是双旋,图片参考左单旋和右单旋的图片。

左右双旋解决办法:进行两次旋转(先左再右),例如:如果插入在左子树的右子树的最右边或者最左边,先进行左单旋,再右单旋。

示例:

单从结果上看:8变成了根结点,8的左结点分给了5子树,右结点分给了10子树。。

左右双旋转情况分析:

场景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和10平衡因子为0,5平衡因子为-1。

场景3:

        h==0时,a/b/c都是空树,b自己就是一个新增结点,不断更新5->10平衡因子,引发旋转,其中8的平衡因子为0,旋转后8和10和5平衡因子均为0。

总结:实际上就分为2种情况,①在左子树的右子树插入;②在左子树的右结点插入。

代码示例:

void RotateLR(Node* parent)//左右双旋
{
	//为更新平衡因子做准备
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;

	RotateL(parent->_left);//左单旋//左子树进行左旋
	RotateR(parent);//右单旋//整颗子树进行右旋

	//更新平衡因子
	if (bf == -1)
	{
		subL->_bf = 0;
		parent->_bf = 1;
		subLR->_bf = 0;
	}
	else if (bf == 0)
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent = 0;
	}
	else if (bf == 1)
	{
		subL->_bf = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}

}

5)右左双旋

        跟左右双旋反过来就行 。就是在右子树的左子树或者左结点插入结点。

情况分析:

场景1

        h>=1时,新增结点插入在e子树,e子树高度从h-1变为h并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为-1,旋转后10和12平衡因子为0,15平衡因子为1。

场景2

        h >= 1时,新增结点插入在f子树,f子树高度从h-1变为 h 并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为1,旋转后15和12平衡因子为0,10平衡因子为-1。

场景3:

        h==0时,a/b/c 都是空树,b 自己就是一个新增结点,不断更新 15->10 平衡因子,引发旋转,其中12的平衡因子为0,旋转后10和12和15平衡因子均为0。

代码示例:

	void RotateRL(Node* parent)//右左双旋
	{
		//为更新平衡因子做准备
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(parent->_right);//右单旋//右子树的进行右旋
		RotateL(parent);//左单旋//整颗子树进行左旋

		//更新平衡因子
		if (bf == -1)
		{
			subR->_bf = 1;
			parent->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			parent->_bf = -1;
		}
		else
		{
			assert(false);
		}

	}

五、AVL树的查找

跟二叉搜索树的一样。

时间复杂度:O(logn)

代码示例:

Node* Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < key)
        {
            cur = cur->_right;
        }
        else if (cur->_kv.first > key)
        {
            cur = cur->_left;
        }
        else
        {
            return cur;
        }
    }

    return nullptr;

}

六、AVL树的检测

        我们实现的AVL树是否合格,我们通过检查左右子树高度差的的程序进行反向验证,同时检查一下结点的平衡因子更新是否出现了问题。

代码:

#define _CRT_SECURE_NO_WARNINGS 1

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 _IsBalanceTree(Node* root)
{
	// 空树也是AVL树
	if (nullptr == root)
		return true;
	// 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;
	// 如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者
	// pRoot平衡因⼦的绝对值超过1,则⼀定不是AVL树
	if (abs(diff) >= 2)
	{
		cout << root->_kv.first << "⾼度差异常" << endl;
		return false;
	}
	if (root->_bf != diff)
	{
		cout << root->_kv.first << "平衡因⼦异常" << endl;
		return false;
	}
	// pRoot的左和右如果都是AVL树,则该树⼀定是AVL树
	return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

// 测试代码
void TestAVLTree1()
{
	AVLTree<int, int> t;
	// 常规的测试⽤例
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	// 特殊的带有双旋场景的测试⽤例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		t.Insert({ e, e });
	}
	t.InOrder();
	cout << t.IsBalanceTree() << endl;
}
// 插⼊⼀堆随机值,测试平衡,顺便测试⼀下⾼度和性能等
void TestAVLTree2()
{
	const int N = 100000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
	}
	size_t begin2 = clock();
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	size_t end2 = clock();
	cout << "Insert:" << end2 - begin2 << endl;
	cout << t.IsBalanceTree() << endl;
	cout << "Height:" << t.Height() << endl;
	cout << "Size:" << t.Size() << endl;
	size_t begin1 = clock();
	// 确定在的值
	/*for (auto e : v)
	{
	t.Find(e);
	}*/
	// 随机值
	for (size_t i = 0; i < N; i++)
	{
		t.Find((rand() + i));
	}
	size_t end1 = clock();
	cout << "Find:" << end1 - begin1 << endl;
}

注意:AVL树的删除这里不做任何介绍,参考书籍:《殷人昆数据结构:用面向对象方法与C++语言描述》中讲解。

完!!


网站公告

今日签到

点亮在社区的每一天
去签到