【C++】---AVL树详解

【C++】---AVL树详解

一、AVL树的概念

1、二叉搜索树的缺点

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

2、AVL树的概念

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

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

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

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

二、AVL树的定义

由于要实现AVL树的增删改查,所以定义AVL树的节点,就需要定义parent,否则插入节点时,不知道要链接到树里面哪个节点下面。

1、AVL树的节点定义(以key_value模型为例)

cpp 复制代码
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 ; // 平衡因子
// 构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_kv(kv)
		,_bf(0)
	{

	}
};

2、AVL树的定义

cpp 复制代码
template<class K,class V>
struct AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
		//构造函数
	AVLTree()
		:_root(nullptr)
	{}
 
	void _Destroy(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_Destroy(root->_left);
		_Destroy(root->_right);
 
		delete root;
	}
 
    //重载operator[]
	V& operator[](const K& key)
	{
		pair<Node*, bool> ret = Insert(make_pair(key, V()));
		return ret.first->_kv.second;
	}
 
	//析构函数
	~AVLTree()
	{
		_Destroy(_root);
		_root = nullptr;
	}
 
private:
	Node* _root;
};

三、AVL树的插入

1、插入节点

(1)插入的时候首先要判断特殊情况,如果这个树为空,我们就要这个插入的节点当做树的根。

(2)如果树不为空,分为以下三种情况:

  1. 插入的kv.first比cur的大,kv往右走
  2. 插入的kv.first比cur的小,kv往左走
  3. 相等,插入失败 (因为 二叉搜索树不允许数据冗余)
cpp 复制代码
	// insert插入
	bool insert(const pair<K, V> kv)
	{
		//1. 处理特殊情况
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;// 用cur来遍历整个树
		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走到要插入的位置了
		cur = new Node(kv);
		// 这就是要保存cur的parent的原因 便于插入后的双向链接
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;// 双向链接
	}

2、控制平衡

(1)更新平衡因子

一个节点的平衡因子是否需要更新,取决于它的左右子树的高度是否发生变化。插入一个节点,如果它的父亲的平衡因子需要更新,那么它所在这条路径的从父亲到根的所有节点的平衡因子都需要更新。因此

①如果新增节点是父亲的左子树,那么parent->_bf--

②如果新增节点是父亲的右子树,那么parent->_bf++

更新后:

    a.如果parent->_bf=0,则停止更新

    b.如果parent->_bf==1||-1,需要继续往上更新(说明以parent为根的子树高度变了,由0变成了1或-1,有可能导致parent的parent的平衡因子=2或=-2)

    c.如果parent->_bf=2||-2已经不平衡了,那么需要旋转处理
cpp 复制代码
		// 更新平衡因子
		while (parent)
		{
			if (cur == parent->_left)
			{
				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)
				{
					LS_rotation(parent);
				}
				// 右单旋
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RS_rotation(parent);
				}
				// 右左双旋
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					/*RS_rotation(parent->_right);
					LS_rotation(parent);*/

					RL_rotation(parent);
				}
				// 左右双旋
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					/*LS_rotation(parent->_left);
					RS_rotation(parent);*/
					LR_rotation(parent);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
		return true;

(2)旋转

旋转处理有4种:右单旋、左单旋、右左单旋、左右单旋

注意:目前以平衡因子 = 右子树的高度 - 左子树的高度 为标准

①右单旋

右单旋顾名思义就是根的左子树高度太高了,需要往右边旋转。

右单旋还有一个特征就是父亲的平衡因子为-2,父亲的左孩子的平衡因子为-1

cpp 复制代码
	// 右单旋
	else if (parent->_bf == -2 && cur->_bf == -1)
	{
		RS_rotation(parent);
	}


右单旋 的 代码块:

cpp 复制代码
// 右单旋
	void RS_rotation(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//
		parent->_left = subLR;
		//
		if (subLR)
			subLR->_parent = parent;

		//
		subL->_right = parent;
		Node* ppNode = parent->_parent;
		parent->_parent = subL;

		//
		if (parent == _root)
		{
			subL = _root;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}
		parent->_bf = subL->_bf = 0;
	}
②左单旋

左单旋顾名思义就是根的右子树高度太高了,需要往左边旋转。

左单旋还有一个特征就是父亲的平衡因子为2,父亲的右孩子的平衡因子为1

cpp 复制代码
// 左单旋
	if (parent->_bf == 2 && cur->_bf == 1)
	{
		LS_rotation(parent);
	}


左单旋 代码:

cpp 复制代码
// 左单旋
	void LS_rotation(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		//
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;
		//
		subR->_left = parent;
		Node* ppNode = parent->_parent;
		parent->_parent = subR;

		//
		if (parent == _root)
		{
			subR = _root;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
		parent->_bf = subR->_bf = 0;
	}
③左右单旋

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

cpp 复制代码
// 左右双旋
	else if (parent->_bf == -2 && cur->_bf == 1)
	{
		/*LS_rotation(parent->_left);
		RS_rotation(parent);*/
		LR_rotation(parent);
	}			



cpp 复制代码
// 左右双旋 : 最主要的是 更新平衡因子
	void LR_rotation(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		// 我们以 subLR 的平衡因子为标准:有三种情况
		// 1、如果在subLR的左边插入 那么最后旋转的结果 :insert的这个subL的右边!
		// 2、如果在subLR的右边插入 那么最后旋转的结果 :insert的这个parent的左边!

		// 3、subLR本身就是自增!

		int bf = subLR->_bf;

		// 旋转
		LS_rotation(parent->_left);
		RS_rotation(parent);
		// 更新平衡因子
		// (1)在subLR的左边插入:bf==-1
		if (bf == -1)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = -1;
		}

		// (2)在subLR的右边插入:bf==1
		else if (bf == 1)
		{
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		// (3)subLR 是自增! bf==0
		else if (bf == 0)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 0;
		}

		else
		{
			assert(false);
		}
	}
④右左单旋

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

cpp 复制代码
// 右左双旋
	else if (parent->_bf == 2 && cur->_bf == -1)
	{
		/*RS_rotation(parent->_right);
		LS_rotation(parent);*/

		RL_rotation(parent);
	}


cpp 复制代码
// 右左双旋 : 最主要的是 更新平衡因子
	void RL_rotation(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		//
		int bf = subRL->_bf;

		RS_rotation(parent->_right);
		LS_rotation(parent);

		if (bf == -1)
		{
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subRL->_bf = 0;
			subR->_bf = 0;
		}
		else
		{
			assert(false);

		}

	}

(3)完整 四个旋转的详细图如下:




(4)完整 更新平衡因子(规律)图解如下:





四、AVL树的查找

查找比较简单:

(1)如果key比当前节点大,那就继续向右查找;

(2)如果key比当前节点小,那就继续向左查找;

(3)如果key恰好等于当前节点,找到了

cpp 复制代码
// find函数
	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_kv.first)
			{
				cur = cur->_left;
			}
			else if (key > cur->_kv.first)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

五、AVL树的高度

计算树高度肯定要递归计算:

(1)计算左右子树的高度

(2)谁的高度大,那AVL树的高度就为较高子树的高度+1

cpp 复制代码
int _Hight(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		return max(_Hight(root->_left), _Hight(root->_right)) + 1;
	}

六、判断是否为AVL树(即:判断是否平衡)

检查树是否是AVL树:

(1)获取左右子树高度

(2)根据左右子树高度计算平衡因子

(3)当平衡因子<=2 || -2时就是平衡的

cpp 复制代码
bool _Is_Blance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}

		int _leftHight = _Hight(root->_left);
		int _rightHight = _Hight(root->_right);
		// 不平衡
		if (abs(_leftHight- _rightHight) >= 2)
		{
			return false;
		}

		// 顺变检查一下 平衡因子是否正确!
		if ((_rightHight - _leftHight) != root->_bf)
		{
			cout << root->_kv.first << endl;
			return false;
		}

		return _Is_Blance(root->_left)
			&& _Is_Blance(root->_right);
	}

七、AVL树的遍历

遍历也很简单:递归遍历左子树和右子树即可

cpp 复制代码
// 中序遍历:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}


// 中序遍历 写在私有上面
	void _InOrder(const Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

八、时间复杂度

AVL树的操作时,需要找到位置,因此时间复杂度为高度次 ,时间复杂度O( l o g 2 n log_2 n log2n)。

相关推荐
可均可可11 分钟前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
杨荧27 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
白子寰33 分钟前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_0138 分钟前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj44 分钟前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT1 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
为将者,自当识天晓地。1 小时前
c++多线程
java·开发语言
-Even-1 小时前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
小政爱学习!1 小时前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript