草庐IT

【进阶数据结构】平衡搜索二叉树 —— AVL树

East-sunrise 2023-04-10 原文

🌈感谢阅读East-sunrise学习分享——[进阶数据结构]AVL树
博主水平有限,如有差错,欢迎斧正🙏感谢有你 码字不易,若有收获,期待你的点赞关注💙我们一起进步🚀


🌈我们上一篇博客分享了搜索二叉树,在文中也铺垫了搜索二叉树的一些结构局限性
而今天分享的一种特殊的搜索二叉树——AVL树,便是一种结构优异的搜索二叉树🎄那么我们就开始吧🚀🚀🚀

目录

一、AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

🎄一棵AVL树可以是一棵空树,或者是一棵具有以下性质的二叉搜索树

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

这里的平衡因子是指:右子树高度-左子树高度

注意:平衡因子只是博主分享的这种实现方法的一种自定义名字(不是必须的),除了使用平衡因子之外还有许多实现AVL树的方法

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(logN),搜索时间复杂度O(logN)


二、AVL树结点的定义

AVL树的结点我们定义了一个三叉链结构,便于后续的操作;并且在每个结点中都引入了平衡因子

template<class K, class V>
struct AVLTreeNode
{
    //存储键值对的pair类
	pair<K, V> _kv;
    
    //含有父节点的三叉链
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

    //平衡因子
	int _bf;

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

//AVL树
template<class K,class V>
struct AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
    //插入
    bool Insert(const pair<K, V>& kv)
    {}
    
private:
	Node* _root = nullptr;
};

三、AVL树的插入

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

  1. 按照二叉搜索树的方式插入新节点
  2. 调整平衡因子,若不平衡则需要旋转调整AVL树

⭕⭕当有新节点插入后我们就需要判断此时的树是否仍然平衡仍然是AVL树了


🚩插入后平衡因子的变化类型?

我们知道,假如平衡,则每个结点的平衡因子只有三种可能:-1,0,1

而插入新结点肯定会使得高度的变化,假如插入新节点后仍平衡,则父节点的平衡因子的变化有:

  • 0 --> 1
  • 0 --> -1
  • 1 --> 0
  • -1 --> 0

知道了平衡因子的变化情况后,又抛出了一个问题

🚩插入新节点影响父节点的平衡因子,那是否会影响祖先结点的平衡因子?

最简单的情况就是插入了新节点,只影响了其父结点,只需更新父节点的平衡因子

插入新节点后,改变了其父结点(8)的子树高度,所以需要更新父节点的平衡因子,但是插入之后并不会改变其祖先结点的子树高度,所以不需要往上更新平衡因子

📌因此我们可以总结出:是否持续更新平衡因子,取决于其结点的子树高度是否变化

再结合一开始的平衡因子变化情况我们可以得出插入新结点后:

  • parent -> _bf == 0 —— 说明之前parent -> _bf 是 1 或者 -1(一边高一边低)新节点刚好插入填上矮的那边,parent所在子树高度不变 —— 祖先的子树高度也不会变 —— 只需更新parent的平衡因子,不需要继续往上更新
  • parent -> _bf == 1 或 -1 —— 说明之前parent -> _bf == 0(两边一样高)新结点插入使得parent所在子树的高度变得一高一低 —— 祖先的子树高度也产生变化 —— 更新parent的平衡因子之外,还需要继续往上更新祖先结点的平衡因子
  • parent -> _bf == 2 或 -2 —— 说明本就一高一低的子树,插入新节点后造成更加不平衡,此时违反了AVL树的平衡规则 —— 就地处理 ——旋转调整

最坏的情况就是插入了新节点,直接影响到了root根结点,所以需要持续更新到root根结点的平衡因子

💭更新结点的平衡因子时,假若我们需要持续向上更新平衡因子,一开始我们更新的是最下面的parent结点,更新后则可向上迭代,直到parent为空就停止

✏️代码实现

bool Insert(const pair<K, V>& kv)
	{
		//空
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		//非空
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				return false;
		}
    
		//插入
		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//调整平衡因子
		while (parent)
		{
			if (cur == parent->_right)
				parent->_bf++;
			else
				parent->_bf--;

			if (parent->_bf == 0)
				break;
			else if (parent->_bf == -1 || parent->_bf == 1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == -2 || parent->_bf == 2)
			{
				//旋转调整
			}
			else
				assert(false);
		}
		return true;
	}

四、AVL树的旋转

🌏如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。

因此旋转的要求即是:

  1. 旋转后仍保持二叉搜索树的结构
  2. 旋转后整棵树保持平衡,平衡因子不超过1

而根据节点插入位置的不同,AVL树的旋转分为四种:

1.左单旋

1️⃣新节点插入较高右子树的右侧 —— 左单旋

此处我们给出左单旋过程的抽象图📌

我们发现,当parent的平衡因子是2,cur是1时,便进行左单旋 ——> 将cur的左子树给parent的右子树,然后将parent及其子树一整棵树变为cur的左子树

左单旋真就如此吗?不信我们可以画出具象图看看

当 h = 0

当 h = 1

当 h = 2

💭有的兄弟看到这就有疑问,为什么h = 2时,子树c一定就得是z的模样呢?
因为假如子树c是x或y的模样,插入新节点时并不会引发节点30的旋转,那样最多只是变成以节点60为parent的树进行左单旋,那就和h = 1是同样的情况了💤因此以上的情况,其实是笼盖了所有需要进行左单旋的子情况了🚩然后以上的情况可能是某棵树的子树

最后我们发现,所有需要进行左单旋的情况,最后的操作都是如一开始所说

✏️代码实现(对照图更清晰易懂)

void RotateL(Node* parent)
{
	Node* subR = parent->_right;//parent的右孩子
	Node* subRL = subR->_left;//parent的右孩子的左孩子
		
    //旋转后subR的左孩子作为parent的右孩子
    parent->_right = subRL;

    //subR的左孩子有可能为空也有可能存在
    //如果存在则需要更新父子关系
	if (subRL)
		subRL->_parent = parent;

    //subR的左孩子变为以parent为根的子树结构
    //同时更新父子关系
    subR->_left = parent;
    parent->_parent = subR;
    
    //parent也可能只是一棵子树的根,其pparent可能为空也可能存在
	Node* pparent = parent->_parent;
	
	if (pparent)
	{
        //如果pparent不为空,则说明parent是一棵子树
        //可能是存在于其父节点的左子树or右子树
		if (parent == pparent->_left)
			pparent->_left = subR;
		else
			pparent->_right = subR;
		subR->_parent = pparent;
	}
	else
	{
        //若pparent为空,则说明parent是整棵树的根节点
        //旋转后根节点已经换人了需要更新
		_root = subR;
		subR->_parent = nullptr;
	}
    //最后更新平衡因子
	parent->_bf = subR->_bf = 0;
}

📌看完以上的代码实现,发现旋转的代码实现起来也有许多细节需要注意啊…

因为旋转后也要保持一棵正常的树的结构,因此那些父子链接关系也需要正确更新

2.右单旋

2️⃣新节点插入较高左子树的左侧 - 右单旋

✏️实现及情况考虑可参考左单旋

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

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

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

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

3.左右双旋

3️⃣新节点插入较高左子树的右侧 - 先左单旋再右单旋

左右双旋我们可以复用上面的左单旋和右单旋的代码🚩但是需要注意的是,左右双旋完各个节点的平衡因子有不同的情况,正是因为左右双旋会因为新节点插入的位置不同而影响不同的旋转结果,因此我们总结出了以下三种情况:

  1. h = 0 —— 节点60即是新插入节点

  2. 新节点插入在b

  3. 新节点插入在c

综上所述,当我们在实现左右双旋时的最后,可根据插入新节点后节点60的平衡因子大小,来确定不同的情况

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右子树
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}

	else if (bf == -1) //新增在sublr左子树
	{
		subL->_bf = 0;
		parent->_bf = 1;
		subLR->_bf = 0;
	}

	else //本身就是新增
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
}

4.右左双旋

4️⃣新节点插入较高右子树的左侧——先右单旋再左单旋

✏️实现及情况考虑可参考左右双旋

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 = 0;
		parent->_bf = -1;
		subRL->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else if(bf == 0)
	{
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
}

五、最终代码展示

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>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

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

	AVLTree()
		:_root(nullptr)
	{}

	

	bool Insert(const pair<K, V>& kv)
	{
		//空
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		//非空
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
				return false;
		}
		//插入

		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//调整平衡因子
		while (parent)
		{
			if (cur == parent->_right)
				parent->_bf++;
			else
				parent->_bf--;

			if (parent->_bf == 0)
				break;
			else if (parent->_bf == -1 || parent->_bf == 1)
			{
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == -2 || parent->_bf == 2)
			{
				//旋转调整
				if (parent->_bf == 2 && cur->_bf == 1)
					RotateL(parent);
				else if (parent->_bf == -2 && cur->_bf == -1)
					RotateR(parent);
				else if (parent->_bf == -2 && cur->_bf == 1)
					RotateLR(parent);
				else if (parent->_bf == 2 && cur->_bf == -1)
					RotateRL(parent);
				else
					assert(false);

				break;
			}
			else
				assert(false);
		}
		return true;
	}

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

		if (subRL)
			subRL->_parent = parent;

		Node* pparent = parent->_parent;
		subR->_left = parent;
		parent->_parent = subR;

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

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

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

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

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

	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右子树
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}

		else if (bf == -1) //新增在sublr左子树
		{
			subL->_bf = 0;
			parent->_bf = 1;
			subLR->_bf = 0;
		}

		else if (bf == 0) //本身就是新增
		{

			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	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 = 0;
			parent->_bf = -1;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if(bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void Inorder()
	{
		_Inorder(_root);
	}
	
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_Inorder(root->_right);
	}

	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int hl = Height(root->_left);
		int hr = Height(root->_right);

		return hl > hr ? hl + 1 : hr + 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)
		{
			cout << "平衡因子异常" << endl;
			return false;
		}

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

private:
	Node* _root = nullptr;
};

🌈🌈写在最后 我们今天的学习分享之旅就到此结束了
🎈感谢能耐心地阅读到此
🎈码字不易,感谢三连
🎈关注博主,我们一起学习、一起进步

有关【进阶数据结构】平衡搜索二叉树 —— AVL树的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 匹配未转义的平衡定界符对 - 2

    如何匹配未被反斜杠转义的平衡定界符对(其本身未被反斜杠转义)(无需考虑嵌套)?例如对于反引号,我试过了,但是转义的反引号没有像转义那样工作。regex=/(?!$1:"how\\"#expected"how\\`are"上面的正则表达式不考虑由反斜杠转义并位于反引号前面的反斜杠,但我愿意考虑。StackOverflow如何做到这一点?这样做的目的并不复杂。我有文档文本,其中包括内联代码的反引号,就像StackOverflow一样,我想在HTML文件中显示它,内联代码用一些spanMaterial装饰。不会有嵌套,但转义反引号或转义反斜杠可能出现在任何地方。

  4. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  5. ruby-on-rails - Nokogiri:使用 XPath 搜索 <div> - 2

    我使用Nokogiri(Rubygem)css搜索寻找某些在我的html里面。看起来Nokogiri的css搜索不喜欢正则表达式。我想切换到Nokogiri的xpath搜索,因为这似乎支持搜索字符串中的正则表达式。如何在xpath搜索中实现下面提到的(伪)css搜索?require'rubygems'require'nokogiri'value=Nokogiri::HTML.parse(ABBlaCD3"HTML_END#my_blockisgivenmy_bl="1"#my_eqcorrespondstothisregexmy_eq="\/[0-9]+\/"#FIXMEThefoll

  6. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  7. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  8. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  9. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  10. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

随机推荐