从C语言到C++_27(AVL树)概念+插入接口实现(四种旋转)

news2024/10/7 2:29:20

目录

1. AVL树的概念

2. AVL树结点和树的定义

3. AVL树的插入(未包含旋转)

4. AVL树的旋转

4.1 右右_左单旋

4.2 左左_右单旋

4.3 左右双旋

4.4 右左双旋

5. AVL树的验证

6. AVL树的删除(了解)和性能

7. AVL树插入验证完整代码

8. AVL树笔试选择题

答案:

本章完。


1. AVL树的概念

前一篇对map / multimap / set / multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中
插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此
map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

二叉搜索树虽然可以提高我们查找数据的效率,但如果插入二叉搜索树的数据是有序或接近有序的,此时二叉搜索树会退化为单支树,在单支树当中查找数据相当于在单链表当中查找数据,效率是很低下的。

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

AVL树可以是一棵空树,也可以是具有以下性质的一棵二叉搜索树:

树的左右子树都是AVL树。

树的左右子树高度之差(简称平衡因子)的绝对值不超过1(-1 / 0 / 1)。

如果一棵二叉搜索树的高度是平衡的,它就是AVL树。如果它有n个结点,

其高度可保持在O(logN),搜索时间复杂度也是O(logN)。

注意: 这里所说的二叉搜索树的高度是平衡的是指,树中每个结点左右子树高度之

差的绝对值不超过1,因为只有满二叉树才能做到每个结点左右子树高度之差均为0。

2. AVL树结点和树的定义

我们这里直接实现KV模型的AVL树,为了方便后续的操作,

这里将AVL树中的结点定义为三叉链结构,并在每个结点当中引入平衡因子,

(右子树高度-左子树高度)。除此之外,还需编写一个构造新结点的构造函数,

由于新构造结点的左右子树均为空树,于是将新构造结点的平衡因子初始设置为0即可。

(平衡因子并不是AVL树所必须的,这里只是其中一种实现方法)

AVLTree.h:

#pragma once

#include <iostream>
using namespace std;

template <class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	pair<K, V> _kv; // 存的键值
	int _bf; // balance factor 平衡因子

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

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

protected:
	Node* _root = nullptr; // 给缺省值直接在初始化列表初始化
};

3. AVL树的插入(未包含旋转)

VL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。

那么AVL树的插入过程可以分为两步:

① 按照二叉搜索树的方式插入新节点

与根结点比较如果比根大就往右子树插入,如果比根小就往左子树插入,

直到走到合适的位置就插入,由于这里是三叉链所以需要处理结点之间的关联关系

② 调整节点的平衡因子

当左右子树的高度发生了变化,那么就需要对父亲及祖先路径上的所有结点的平衡因子进行调整

插入结点后需要倒着往上更新平衡因子,更新规则如下:

1. 新增结点在parent的右边,parent的平衡因子+ +。

2. 新增结点在parent的左边,parent的平衡因子− −。

每更新完一个结点的平衡因子后,都需要进行以下判断:

如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。

如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。

如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,

需要进行旋转处理。

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

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur) // 找要插入的位置
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->right;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (kv.first < parent->_kv.first) // 插入要插入的位置
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent; // 三叉链多一步

		while (parent) // 控制平衡, 更新平衡因子, 如果平衡因子不对, 就要旋转
		{
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			if (parent->_bf == 0)
			{
				break;
			}
			else if (abs(parent->_bf) == 1) // 往上更新
			{
				parent = parent->_parent;
				cur = cur->_parent;
			}
			else if (abs(parent->_bf) == 2) // 不平衡了,需旋转
			{
				// 后面写
			}
			else // 理论不可能走到这,除非之前就错了
			{
				assert(false); // 报个错
			}
		}
		return true;
	}

4. AVL树的旋转

若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,

这时我们需要对以该结点为根结点的树进行旋转处理,而旋转处理分为四种,

在进行分类之前我们首先需要进行以下分析:

当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

理由如下:

若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,

否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。

而cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,

因为新增结点最终会插入到一个空树当中,在新增结点插入前,

其父结点的状态有以下两种可能:其父结点是一个左右子树均为空的叶子结点,

其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。

其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,

新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,

新增结点后其平衡因子更新为0。

综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

根据此结论,我们可以将旋转处理分为以下四类:

  1. 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。
  2. 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
  3. 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
  4. 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。

并且,在进行旋转处理后就无需继续往上更新平衡因子了,

因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,

也就不会影响其父结点的平衡因子了。具体原因请看后面的旋转讲解。

4.1 右右_左单旋

可以看到,经过左单旋后,树的高度变为插入之前了,即树的高度没有发生变化,

所以左单旋后无需继续往上更新平衡因子。

 左单旋的步骤如下:

  1. 让subR的左子树作为parent的右子树。
  2. 让parent作为subR的左子树。
  3. 让subR作为整个子树的根。
  4. 更新平衡因子。

左单旋后满足二叉搜索树的性质:

  1. subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
  2. parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。

左单旋代码:

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right; // 动了三个标记了的结点,共更新六个指针,这更新两个指针
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL) // subRL不为空才更新
		{
			subRL->_parent = parent;
		}

		Node* ppNode = parent->_parent; // 记录parent的parent,防止parent是一颗子树的头结点

		subR->_left = parent; // 再更新两个指针
		parent->_parent = subR;

		if (_root == parent)  // 最后更新两个指针
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else // parent是一颗子树的头结点
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}

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

4.2 左左_右单旋

经过右单旋后,树的高度变为插入之前了,即树的高度没有发生变化,

所以右单旋后无需继续往上更新平衡因子。

 右单旋的步骤如下:

  1. 让subL的右子树作为parent的左子树。
  2. 让parent作为subL的右子树。
  3. 让subL作为整个子树的根。
  4. 更新平衡因子。

右单旋后满足二叉搜索树的性质:

  1. subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
  2. parent及其右子树当中结点的值本身就比subL的值大,因此可以作为subL的右子树。

右单旋代码:

	void RotateR(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 (_root == parent) // 最后更新两个结点
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			} 

			subL->_parent = ppNode;
		}

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

4.3 左右双旋

左右双旋步骤示意图

1、插入新结点。这里可能插入到b / c下面,还可能h等于0,插入到60下面

(以上三种插入的后两步都是一样的,只是最后平衡因子不同:)

 2、以30为旋转点进行左单旋。

 3、以90为旋转点进行右单旋。

左右双旋的步骤如下:

  1. 以subL为旋转点进行左单旋。(前两步都可以复用上面的代码)
  2. 以parent为旋转点进行右单旋。
  3. 更新平衡因子。(左右双旋复杂的地方)

左右双旋后满足二叉搜索树的性质:

左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subL和parent的右子树和左子树,再让subL和parent分别作为subLR的左右子树,最后让subLR作为整个子树的根(结合图理解)。

1. subLR的左子树当中的结点本身就比subL的值大,因此可以作为subL的右子树。

2. subLR的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。

3. 经过步骤1/2后,subL及其子树当中结点的值都就比subLR的值小,而parent及其子树当中结点的值都就比subLR的值大,因此它们可以分别作为subLR的左右子树。

观察发现,左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:

1、当subLR原始平衡因子是-1时,左右双旋后subLR,parent、subL的平衡因子分别更新为0、1、0。

2、当subLR原始平衡因子是1时,左右双旋后subLR、parent、subL的平衡因子分别更新为0、0、-1。

3、当subLR原始平衡因子是0时,左右双旋后subLR、parent、subL的平衡因子分别更新为0、0、0。

 可以看到,经过左右双旋后,树的高度变为插入之前了,即树的高度没有发生变化,

所以左右双旋后无需继续往上更新平衡因子。

左右双旋代码:

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left; // 记录subL的平衡因子
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateL(parent->_left);
		RotateR(parent);

		subLR->_bf = 0; // 三种情况一样
		if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
		}
		else // 理论不应走到这
		{
			assert(false); //在旋转前树的平衡因子就有问题,报错
		}
	}

4.4 右左双旋

思路和左右双旋一样,这里看图就行

右左双旋步骤示意图

1、插入新结点。 

2、以90为旋转点进行右单旋。

3、以30为旋转点进行左单旋。

 右左双旋的步骤如下:

  1. 以subR为旋转点进行右单旋。
  2. 以parent为旋转点进行左单旋。
  3. 更新平衡因子。

右左双旋后满足二叉搜索树的性质:

右左双旋后,实际上就是让subRL的左子树和右子树,分别作为parent和subR的右子树和左子树,再让parent和subR分别作为subRL的左右子树,最后让subRL作为整个子树的根(结合图理解)。

subRL的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。

subRL的右子树当中的结点本身就比subR的值小,因此可以作为subR的左子树。

经过步骤1/2后,parent及其子树当中结点的值都就比subRL的值小,

而subR及其子树当中结点的值都就比subRL的值大,因此它们可以分别作为subRL的左右子树。
 

右左双旋后,平衡因子的更新随着subRL原始平衡因子的不同分为以下三种情况:

1、当subRL原始平衡因子是1时,左右双旋后subRL、parent、subR的平衡因子分别更新为

0、-1、0。

2、当subRL原始平衡因子是-1时,左右双旋后subRL、parent、subR的平衡因子分别更新为

0、0、1。

3、当subRL原始平衡因子是0时,左右双旋后subRL、parent、subR的平衡因子分别更新为

0、0、0。

 

经过右左双旋后,树的高度变为插入之前了,即树的高度没有发生变化,

所以右左双旋后一样无需继续往上更新平衡因子。 

 右左双旋代码:

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right; // 记录subL的平衡因子
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		subRL->_bf = 0; // 三种情况一样
		if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
		}
		else // 理论不应走到这
		{
			assert(false); //在旋转前树的平衡因子就有问题,报错
		}
	}

5. AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,

因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。

	void InOrder()
	{
		_InOrder(_root)
	}

protected:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

但中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,

在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。

采用后序遍历,变量步骤如下:

从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)

求高度函数以前写过了:

	int Height(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		return max(Height(root->_left), Height(root->_right)) + 1;
	}

先判断左子树是否是平衡二叉树。再判断右子树是否是平衡二叉树。

若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,

继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。

(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)

	bool IsBalance()
	{
		return _IsBalance(_root);
	}

protected:
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHT = Height(root->_left);
		int rightHT = Height(root->_right);
		int diff = rightHT - leftHT;

		if (diff != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			cout << rightHT << " - " << leftHT << endl;
			return false;
		}

		return abs(diff) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

6. AVL树的删除(了解)和性能

AVL树的删除(了解)

AVL树的删除和其它接口这里不实现了,如果不是AVL树会考到,是不用学的,

因为实际可以用接下来学的红黑树替代它。

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,

然后再更新平衡因子,只不错与删除不同的时,删除节点后的平衡因子更新,

最差情况下一直要调整到根节点的位置。

具体实现参考《数据结构 - 用面向对象方法与C++描述》殷人昆版。

AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,

这样可以保证查询时高效的时间复杂度,即O(logN)。

但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:

插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,

有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,

而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

7. AVL树插入验证完整代码

AVLTree.h:

#pragma once

#include <iostream>
#include <assert.h>
#include <algorithm>
#include <time.h>
using namespace std;

template <class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	pair<K, V> _kv; // 存的键值
	int _bf; // balance factor 平衡因子

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

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

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

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

		cur = new Node(kv);
		if (kv.first < parent->_kv.first) // 插入要插入的位置
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent; // 三叉链多一步

		while (parent) // 控制平衡, 更新平衡因子, 如果平衡因子不对, 就要旋转
		{
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			if (parent->_bf == 0)
			{
				break;
			}
			else if (abs(parent->_bf) == 1) // 往上更新
			{
				parent = parent->_parent;
				cur = cur->_parent;
			}
			else if (abs(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 InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	bool IsBalance()
	{
		return _IsBalance(_root);
	}

protected:
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int leftHT = Height(root->_left);
		int rightHT = Height(root->_right);
		int diff = rightHT - leftHT;

		if (diff != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			cout << rightHT << " - " << leftHT << endl;
			return false;
		}

		return abs(diff) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

	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;
		}
		return max(Height(root->_left), Height(root->_right)) + 1;
	}

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right; // 动了三个标记了的结点,共更新六个指针,这更新两个指针
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL) // subRL不为空才更新
		{
			subRL->_parent = parent;
		}

		Node* ppNode = parent->_parent; // 记录parent的parent,防止parent是一颗子树的头结点

		subR->_left = parent; // 再更新两个指针
		parent->_parent = subR;

		if (_root == parent)  // 最后更新两个指针
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else // parent是一颗子树的头结点
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}

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

	void RotateR(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 (_root == parent) // 最后更新两个结点
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			} 

			subL->_parent = ppNode;
		}

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

	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left; // 记录subL的平衡因子
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateL(parent->_left);
		RotateR(parent);

		subLR->_bf = 0; // 三种情况一样
		if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
		}
		else // 理论不应走到这
		{
			assert(false); //在旋转前树的平衡因子就有问题,报错
		}
	}

	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right; // 记录subL的平衡因子
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		subRL->_bf = 0; // 三种情况一样
		if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
		}
		else // 理论不应走到这
		{
			assert(false); //在旋转前树的平衡因子就有问题,报错
		}
	}

	Node* _root = nullptr; // 给缺省值直接在初始化列表初始化
};

Test.c:

#include "AVLTree.h"

void TestAVLTree1()
{
	//int arr[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };  // 测试单旋平衡因子调节
	int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };  // 测试双旋平衡因子调节
	AVLTree<int, int> t1;
	for (const auto& e : arr)
	{
		t1.Insert(make_pair(e, e));
	}

	t1.InOrder();
	cout << "IsBalance:" << t1.IsBalance() << endl;
} 

void TestAVLTree2()
{
	size_t N = 10000;
	srand(time(0));
	AVLTree<int, int> t1;
	for (size_t i = 0; i < N; ++i)
	{
		int x = rand();
		t1.Insert(make_pair(x, i));
		//bool ret = t1.IsBalance();
		//if (ret == false)
		//{
		//	int u = 1; // 查bug打断点用
		//}
		//else
		//{
		//	cout << "Insert:" << x << " IsBalance:" << ret << endl;
		//}
	}
	cout << "IsBalance:" << t1.IsBalance() << endl;
}

int main()
{
	TestAVLTree1();

	return 0;
}

8. AVL树笔试选择题

1. 下面关于AVL树说法不正确的是()

A.AVL树也是二叉搜索树

B.极端情况下,AVL树可能也会退化成单支树

C.AVL查询的时间复杂度是O(log_2N)

D.AVL树是通过平衡因子限制保证其平衡性的

2. 现有一棵无重复关键字的平衡二叉树(AVL树),对其进行中序遍历可得到一个降序序列。

下列关于该平衡二叉树的叙述中,正确的是()

A.根结点的度一定为2

B.树中最小元素一定是叶结点

C.最后插入的元素一定是叶结点

D.树中最大元素一定是无左子树

3. 关于AVL树的旋转说法正确的是()

A.插入时,AVL树最多只需要旋转两次

B.删除时,只要某个节点的平衡因子不满足特性时 ,只需要对该棵子树进行旋转,就可以使AVL树再次平衡

C.AVL树的节点中必须维护平衡因子,因为要依靠其平衡因子是否需要旋转以维护其平衡性

D.AVL树的双旋转只需要直接使用对应的单旋转即可

答案:

1. B

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

   1. 它的左右子树都是AVL树

   2. 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

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

  A:正确,参考上述概念

  B:错误,AVL树没有极端情况,其是为了防止二叉搜索树的极端情况二给出的

  C:正确,参考上述概念

  D:正确,平衡因子:左右子树高度之间,其绝对值如果不超过1,则认为树就是平衡的

2. D

题目中说:中序遍历得到一个降序序列,则说明:根小于左子树中节点,大于右子树中节点

  A:错误,根可以没有左子树,比如树中只有两个节点,即根以及根的右子树

  B:错误,树中最小的元素一定是最左侧或者最右侧节点,但不一定是叶子节点

  C:错误,最后插入的元素不一定是叶子节点,因为新节点插入后,为了保证其平衡性,还要对树 进行旋转处理,旋    转之后,就不一定在叶子的位置

  D:正确,因为最大元素如果存在左子树,中序遍历就不可能是降序序列

3. A

  A:正确,即双旋

  B:错误,可能需要旋转多次,子树旋转后,其高度降低了一层,其上层可能也需要跟着旋转

  C:错误,平衡因子不是必须要维护的,在操作时也可以直接通过高度函数来算,只不过比较麻烦

  D:错误,不能直接使用单旋转,因为两个单旋转完成后,还需要对部分节点的平衡因子进行更新

本章完。

下一篇:红黑树概念和实现。然后是set和map的模拟实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/775650.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

下载pdm遇到的坑:Could not find a version that satisfies the requirement pdm

pip install pdm遇到的问题&#xff1a; Could not find a version that satisfies the requirement pdm (from versions: ) No matching distribution found for pdm检查了版本后发现&#xff0c;python版本在3.6&#xff0c;pdm不支持该版本 换成python3.7版本&#xff0c;则…

【力扣算法14】之 15. 三数之和 python

文章目录 问题描述示例1示例2示例 3提示 思路分析代码分析完整代码详细分析运行效果截图调用示例运行结果 完结 问题描述 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] num…

python编程语言之数据类型进阶操作

数值常用操作 python常用关于数值&#xff0c;数学常用的模块&#xff1a;math&#xff08;数学&#xff09;&#xff0c;random&#xff08;随机&#xff09;&#xff0c;numpy&#xff08;科学计算&#xff09;&#xff0c;pandas&#xff08;数据读写&#xff0c;数据分析&…

Yalmip入门教程(3)-约束条件的定义

博客中所有内容均来源于自己学习过程中积累的经验以及对yalmip官方文档的翻译&#xff1a;https://yalmip.github.io/tutorials/ 之前的博客简单介绍了约束条件的定义方法&#xff0c;接下来将对其进行详细介绍。 首先简单复习一下&#xff1a; 1.定义约束条件可以使用矩阵拼接…

如何通过nvm管理多个nodejs版本

随着前端项目的越来越多&#xff0c;不同项目使用的nodejs版本可能不一样&#xff0c;导致在切换不同项目时需要更换不同的nodejs版本&#xff0c;非常麻烦。本次推荐使用nvm进行多个nodejs版本的统一管理。 1、nvm的下载 nvm全称Node Version Manager&#xff0c;即Node版本管…

科技政策 | 2023年广东省省级企业技术中心(第22批)认定开始啦!

原创 | 文 BFT机器人 原文链接&#xff1a; http://gdii.gd.gov.cn/zwgk/tzgg1011/content/post_4218083.html 各企业请注意&#xff0c;2023年广东省省级企业技术中心&#xff08;第22批&#xff09;认定已经开始了&#xff0c;广东省工业和信息化厅接收资料截止时间为2023年…

【Java基础教程】Java学习路线攻略导图——史诗级别的细粒度归纳,持续更新中 ~

Java学习路线攻略导图 上篇 前言1、入门介绍篇2、程序基础概念篇3、包及访问权限篇4、异常处理篇5、特别篇6、面向对象篇7、新特性篇8、常用类库篇 前言 &#x1f37a;&#x1f37a; 各位读者朋友大家好&#xff01;得益于各位朋友的支持和关注&#xff0c;我的专栏《Java基础…

Python实战项目——物流行业数据分析(二)

今天我们对物流行业数据进行简单分析&#xff0c;数据来源&#xff1a;某企业销售的6种商品所对应的送货及用户反馈数据 解决问题&#xff1a; 1、配送服务是否存在问题 2、是否存在尚有潜力的销售区域 3、商品是否存在质量问题 分析过程&#xff1a; 依旧先进行数据处理 一…

vue Duplicate keys detected: ‘‘. This may cause an update error. found in

错误原因&#xff1a; 在使用v-for的时候&#xff0c;都要必须加上一个唯一的key值&#xff0c;key的值写成一样的了。所以就导致了警告。尽量不要使用index下标作为key值 换成后台数据返回的id或者i*随机数作为key值就好

linux中快速定位软件安装位置

linux中快速定位软件安装位置步骤如下&#xff1a; 根据进程的名字定位进程ID ps -ef | grep redis通过进程id查找软件安装位置 ll -l /proc/100788/cwd原理说明&#xff1a; linux中进程启动后&#xff0c;会在/proc/目录下新建进程工作目录; 目录规范为&#xff1a;/proc/…

寻找下一个生成式 AI 独角兽,亚马逊云科技创业加速器火热招募中!

生成式AI让人工智能技术又一次破圈&#xff0c;带来了机器学习被大规模采用的历史转折点。它正在掀起新一轮的科技革命&#xff0c;为人类带来前所未有的颠覆性的影响&#xff0c;而诸多创业者也应势而上&#xff0c;寻求创新机遇。生成式AI可以创造全新的客户体验、提高企业内…

将数字孪生系统接入 CesiumJS,能为智慧城市项目带来怎样的改变?

数字孪生系统接入 CesiumJS 的契机&#xff0c;正是智慧城市项目的需要。因为许多智慧城市项目中包含了大量地形、倾斜摄影、DOM、DEM 等 GIS 数据&#xff0c;那么为了能够在数字孪生系统中导入这些 GIS 数据&#xff0c;同时让这些数据在以可视化形式表现出来后&#xff0c;还…

C++-17. 电话号码的字母组合

题目来源&#xff1a;力扣 题目描述&#xff1a; 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输…

【MySQL】数据不存在则插入

系列文章 C#底层库–MySQLBuilder脚本构建类&#xff08;select、insert、update、in、带条件的SQL自动生成&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/129179216 C#底层库–MySQL数据库操作辅助类&#xff08;推荐阅读&#xff0…

全面认识二极管,一篇文章就够了

电子设计基础元器件 二极管&#xff0c;小小二极管&#xff0c;大大用途。 ... 矜辰所致目录 前言一、二极管基础知识1.1 什么是二极管1.2 二极管的组成1.3 二极管的原理 二、二极管特性2.1 伏安特性曲线图2.2 温度的影响2.3 关于击穿 三、 二极管的参数正向连续电流和平均整…

银行金融风险管理面试问题汇总(附答案)

最近有些学员在咨询换工作的事&#xff0c;包括一些金融上市公司的高管。我收集了一些金融风险管理面试问题相关资料&#xff0c;希望能帮助大家。记得收藏此文章&#xff0c;以防之后找不到文章。 风险经理识别和分析潜在的公司风险&#xff0c;并找到减少或避免风险的方法。…

pve (群辉、软路由、win/linux)折腾日记

目录 生命不息&#xff0c;折腾不止名词解释硬件参数装机PVE安装下载pveultraISO 把镜像写入u盘rufus把镜像写入U盘bios设置U盘启动安装pve系统 ssh连接pvepve的使用安装pvetools安装ubuntu-server系统ubuntu更换国内源ubuntu安装docker更改docker国内源docker环境下安装青龙 安…

了解 MySQL 的存储引擎

点击上方↑“追梦 Java”关注&#xff0c;一起追梦&#xff01; 存储引擎的主要工作就是与文件系统进行数据交互&#xff0c;比如我们常用的 InnoDB 引擎。 MySQL 的存储引擎是插件式的&#xff0c;应用程序无需针对不同的存储引擎进行对应的编码操作&#xff0c;MySQL 提供了一…

什么是布道师?看完这篇文章你就懂了

布道师这个术语可能对许多人来说还比较陌生&#xff0c;但实际上&#xff0c;布道师在软件行业中扮演着非常重要的角色。他们是软件产品的积极倡导者和用户之间的桥梁&#xff0c;致力于传递好消息、收集反馈&#xff0c;并与用户建立良好的关系。在本文中&#xff0c;我们将深…

Linux —— 查看进程命令及进程优先级

目录 一&#xff0c;查看进程命令 1&#xff0c;ps 命令 ps axj ps aux ps l ps -l 2&#xff0c;top 命令 3&#xff0c;ptree 命令 4&#xff0c;pgrep 命令 三&#xff0c;进程优先级 PRI NI 一&#xff0c;查看进程命令 ps、top、pstree、grep&#xff1b; 1&…