平衡二叉树(AVL)讲解二

平衡二叉树概念回顾

3.删除(Delete)

删除是AVL树最复杂的操作,因为它可能导致多个祖先节点连续失衡,修复可能一直传播到根节点。

第一步:标准BST删除。分三种情况处理待删节点:

1.无子节点:直接删除。

2.只有一个子节点:用子节点顶替自己。

3.有两个子节点:找到中序后继(右子树的最左节点)或中序前驱(左子树的最右节点)来替换值,然后递归删除那个后继/前驱节点(该节点最多只有一个子节点)。

源码见二叉搜索树(BST Tree)讲解

第二步:计算平衡因子 BF = height(left) - height(right)。从实际被删除节点的父节点开始,自底向上回溯到根节点,更新所有经过节点的平衡因子。

如果 BF 为 -1、0、1:说明平衡,但先别急着停,更新完高度后继续向上回溯(因为底层高度变了,可能影响祖先)。

如果 BF 为 +2 或 -2:失衡了! 必须旋转修复。

第三步:检查并修复失衡。在回溯过程中,每遇到一个失衡节点,就执行旋转修复。与插入不同,删除后的旋转会降低子树的高度,这可能导致其祖先节点再次失衡。因此,删除操作在修复一个节点后,必须继续向上回溯检查,直到根节点。

假设失衡节点为 X(BF = ±2),看 X 的子节点(Y)的平衡因子来决定用哪种旋转。

情况 A:BF(X) = +2(左子树比右子树高)

说明问题出在左子树上,此时看左子节点 Y = X.left 的平衡因子:

Y 的 BF 失衡类型 执行操作 口诀

≥ 0(即 +1 或 0) LL 对 X 执行右旋 左左向右转

< 0(即 -1) LR 先对 Y 执行左旋,再对 X 执行右旋 左右先左后右

情况 B:BF(X) = -2(右子树比左子树高)

说明问题出在右子树上,此时看右子节点 Y = X.right 的平衡因子:

Y 的 BF 失衡类型 执行操作 口诀

≤ 0(即 -1 或 0) RR 对 X 执行左旋 右右向左转

0(即 +1) RL 先对 Y 执行右旋,再对 X 执行左旋 右左先右后左

第四步:决定是否继续向上回溯(删除最关键的区别)

执行完旋转修复后,你需要检查以 X 为根的子树的新高度,和删除前(或旋转前)的高度是否发生了变化:

如果高度降低了:这说明变矮了,必须继续向上回溯,检查 X 的父节点是否因此失衡(因为子树变矮会导致父节点的平衡因子改变)。

如果高度没变:说明这棵子树的高度和删除前一样,可以立即停止回溯,修复结束。

通俗理解:删除就像一个"抽砖块"。如果抽掉砖块导致这一层变矮了,那上面的楼层受力会变,必须一层层查上去;如果抽掉后找了块别的砖垫上,总高度没变,那楼上的结构就安全了。

关键特性:删除操作可能触发 O(log n) 次旋转,因为修复了一个失衡点,高度降低后,上面的节点可能又失衡了。

cpp 复制代码
//获取当前节点的高度
int BinarySearchTree::treeHeight(const Node * node)
{
	if (node == nullptr)
	{
		return 0;
	}
	int l = treeHeight(node->left);
	int r = treeHeight(node->right);

	return 1 + (l > r ? l : r);
}

//平衡修复
bool AVLTree::reBalance(Node * node)
{
	if (node == nullptr)
	{
		return true;
	}

	Node* right = node->right;
	Node* left = node->left;
	int r = treeHeight(right);
	int l = treeHeight(left);
	if (r - l > 1)
	{
		//右高左低
		if (right->right)
		{
			//右右型
			levorotation(node, right);
			return reBalance(right);
		}
		else
		{
			//右左型 先右旋后左旋
			Node* s = right->left;
			rightHandedRotation(right, s);
			levorotation(node, s);
			return reBalance(s);
		}
	}
	else if (l - r > 1)
	{
		//左高右低
		if (left->left)
		{
			//右旋
			rightHandedRotation(node, left);
			return reBalance(left);
		}
		else
		{
			//只有右子树 为左右型 先左旋后右旋
			Node* s = left->right;
			levorotation(left, s);
			rightHandedRotation(node, s);
			return reBalance(s);
		}
	}
	else
	{
		return reBalance(node->parent);
	}
	return false;
}

Node * AVLTree::remove(int val)
{
	//二叉查找树的删除
	Node* node = BinarySearchTree::remove(val);

	//修复平衡
	reBalance(node);
	return node;
}

按照树的格式输出

cpp 复制代码
void AVLTree::printTree()
{
	if (rootNode == nullptr)
	{
		return;
	}

	std::vector<Node*> vec;
	vec.push_back(rootNode);
	int count = 0;
	int index = 0;
	int h = 5 * std::pow(2, treeHeight(rootNode));
	int s = 1;
	while (true)
	{
		count = vec.size();

		if (index == count)
		{
			break;
		}

		std::string str = "";
		char spli = '_';
		bool isFlag = false;
		for (int i = index; i < count; i++)
		{
			Node* node = vec[i];
			str.append(h / s, spli);
			if (node)
			{
				str += std::to_string(node->val);
				isFlag = true;
			}
			else
			{
				str += "*";
			}
			str.append(h / s, spli);
			if (node)
			{
				vec.push_back(node->left);
				vec.push_back(node->right);
			}
		}
		if (isFlag)
		{
			std::cout << str << std::endl;
			std::cout << std::endl;
		}

		index = count;
		s *= 2;
	}
}

4.查找后继(中序后继)

要找节点 x 的后继,分两种情况:

情况一:节点 x 有右子树

策略:往右走一步,然后一路向左到底。

因为右子树里的所有节点都比 x 大,其中最小的那个肯定在右子树的最左下角。

步骤:x.right -> 然后不断往左走(node.left),直到没有左孩子为止。

情况二:节点 x 没有右子树

策略:向上找祖先,直到"拐弯"为止。

因为没有右子树,说明比 x 大的节点只能在它的祖先里。我们要找到第一个向左拐的祖先(即,该祖先的左子树包含了节点 x)。

步骤:让指针指向 x,不断向上找父节点,当发现当前节点是其父节点的左孩子时,那个父节点就是后继。

源码实现

cpp 复制代码
bool AVLTree::successor(int val, int & res)
{
	//当前AVL树为空
	if (rootNode == nullptr)
	{
		return false;
	}

	Node* node;
	//未找到当前结点
	if (!find(val, &node))
	{
		return false;
	}

	if (node->right)
	{
		//在右子树中找最小的
		if (node->right->left)
		{
			node = node->right->left;
		}
		else
		{
			node = node->right;
		}
		while (node->left)
		{
			node = node->left;
		}

		res = node->val;
		return true;
	}

	//当前结点没有右孩子
	if (node == rootNode)	//当前是根结点
	{
		return false;
	}

	//当前结点是左孩子,父节点就是它的后继
	//当前结点是右孩子,找第一个是左孩子的结点的父节点
	while (node && node->parent && !node->isLeftTree)//当前结点不为空,结点的父节点不为空,当前结点是右节点,继续查找第一个左节点
	{
		node = node->parent;
	}
	
	if (node->isLeftTree)
	{
		res = node->parent->val;
		return true;
	}
	return false;
}

查找前驱(中序前驱)

逻辑和后继完全对称,把"左右"反过来即可。

情况一:节点 x 有左子树

策略:往左走一步,然后一路向右到底。

左子树都比 x 小,其中最大的那个肯定在左子树的最右下角。

步骤:x.left -> 然后不断往右走(node.right),直到没有右孩子为止。

情况二:节点 x 没有左子树

策略:向上找祖先,直到"拐弯"为止。

向上找第一个向右拐的祖先(即,该祖先的右子树包含了节点 x)。

步骤:让指针指向 x,不断向上找父节点,当发现当前节点是其父节点的右孩子时,那个父节点就是前驱。

源码实现

cpp 复制代码
bool AVLTree::predecessor(int val, int & res)
{
	Node* node = nullptr;
	if (!find(val, &node))
	{
		return false;
	}

	//当前结点有左结点
	if (node->left)
	{
		//当前结点的左孩子没有右孩子,左孩子最大值就是左子树的根节点
		if (node->left->right == nullptr)
		{
			res = node->left->val;
			return true;
		}

		//当前结点的左孩子有右孩子,在右孩子中找最大值
		node = node->left->right;
		while (node->right)
		{
			node = node->right;
		}
		res = node->val;
		return true;
	}

	//当前结点没有左孩子
	if (node == rootNode)//当前结点是根结点
	{
		return false;
	}

	//如果当前结点是右孩子,则父结点为前驱
	//如果当前结点是左孩子,则找到第一个为右孩子的父节点,其祖父结点为前驱
	while (node && node->parent && node->isLeftTree)//当前结点不为空,结点的父节点不为空,当前结点是左节点,继续查找第一个右节点
	{
		node = node->parent;
	}
	
	if (!node->isLeftTree)
	{
		res = node->parent->val;
		return true;
	}
	return false;
}

平衡二叉树的测试程序

cpp 复制代码
//AVL树测试
void BSTTest()
{
	AVLTree bst;
	//创建树
	std::cout << "插入节点:7" << std::endl;
	bst.create(7);
	bst.printTree();
	std::cout << std::endl;

	std::vector<int> vec{ 5,8,3,4,10 };
	for (size_t i = 0; i < vec.size(); i++)
	{
		//向树中插入结点
		bst.insert(vec[i]);
		std::cout << "插入节点:" << vec[i] << std::endl;
		bst.printTree();
		std::cout << std::endl;
	}

	bst.remove(8);
	std::cout << "删除节点:8" << std::endl;
	bst.printTree();
	std::cout << std::endl;

	bst.remove(3);
	std::cout << "删除节点:3" << std::endl;
	bst.printTree();
	std::cout << std::endl;

	bst.remove(10);
	std::cout << "删除节点:10" << std::endl;
	bst.printTree();
	std::cout << std::endl;

	bst.remove(4);
	std::cout << "删除节点:4" << std::endl;
	bst.printTree();
	std::cout << std::endl;

	bst.remove(7);
	std::cout << "删除节点:7" << std::endl;
	bst.printTree();
	std::cout << std::endl;
}

结果截图

复杂度总结对比

操作 时间复杂度 旋转次数

查找 O(log n) 0

插入 O(log n) 最多 2 次(常数级)

删除 O(log n) 最坏情况 O(log n) 次

勘误纠错

在平衡二叉树讲解中,对AVL的旋转和插入操作源码实现中有误,错误源码没有删除,在错误源码下方更新了正确源码,请客官查收平衡二叉树讲解

相关推荐
知无不研2 小时前
std::function在使用时遇到的问题
c++·算法·st·function
码场老菜鸟3 小时前
C++ 指针的深度理解与应用
java·c++·算法
OPEN-F3 小时前
C++并发编程:条件变量与原子操作
java·开发语言·c++
神仙别闹3 小时前
基于 C++实现(控制台)车辆信息管理系统
开发语言·c++
fpcc3 小时前
数据结构和算法—最短路径的算法
数据结构·算法
重生之小比特3 小时前
【初阶C++】string
开发语言·c++
Dream Cosmos4 小时前
C++继承详解(上):继承规则、父子类转换与作用域隐藏
c++
skr爱码士4 小时前
15_国际化和本地化:tr()、ts 文件、QM 文件、多语言切换
c++·qt·ui·客户端
汉克老师4 小时前
CSP-J 初赛(以满分为目标):第二十一课 《树与二叉树基础—— 一棵树,为什么会成为计算机最重要的数据结构之一?》
c++·csp-j·小学生·学c++编程