暴力数据结构——AVL树

1.认识AVL树

AVL树最先发明的⾃平衡⼆叉查找树,AVL可以是⼀颗空树,或者具备下列性质的⼆叉搜索树:

它的左右⼦树都是AV树,且左右⼦树的⾼度差的绝对值不超过1

AVL树是⼀颗⾼度平衡搜索⼆叉树, 通过控制⾼度差去控制平衡

AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在logN,那么增删查改的效率也可以控制在O(logN),相⽐⼆叉搜索树有了本质的提升
平衡因子:为了在插入时更加方便可以对每个节点设置一个平衡因子,当然这不是必要的但是可以提升效率,平衡因子这里的概念是:该节点的右子树高度 - 该节点左子树高度

2.AVL树的结构

cpp 复制代码
//节点
template<class K, class V>
struct AVLTreeNode
{
      //需要parent指针,后续更新平衡因⼦可以看到 
      pair<K, V> _kv;
      AVLTreeNode<K, V>* _left;
      AVLTreeNode<K, V>* _right;
      AVLTreeNode<K, V>* _parent;
      int _bf; // balance factor
      AVLTreeNode(const pair<K, V>& kv)
      :_kv(kv)
      , _left(nullptr)
      , _right(nullptr)
      , _parent(nullptr)
      ,_bf(0)
      {}
};

//整棵树
template<class K, class V>
class AVLTree
{
   typedef AVLTreeNode<K, V> Node;
public:
private:
   Node* _root = nullptr;
};

3.AVL树的插入

AVL树插⼊⼀个值的⼤概过程

1. 插⼊⼀个值按⼆叉搜索树规则进⾏插⼊

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

3. 更新平衡因⼦过程中没有出现问题,则插⼊结束

4. 更新平衡因⼦过程中出现不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树的⾼度,不会再影响上⼀层,所以插⼊结束

cpp 复制代码
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;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;
	// 更新平衡因⼦ 
	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)
		{
			// 不平衡了,旋转处理 
			break;
		}
		else
		{
			assert(false);
		}
	}
	return true;
}

3.1平衡因子更新

• 更新后parent的平衡因⼦等于0,更新中parent的平衡因⼦变化为-1->0或者1->0,说明更新前 parent⼦树⼀边⾼⼀边低,新增的结点插⼊在低的那边,插⼊后parent所在的⼦树⾼度不变,不会影响parent的⽗亲结点的平衡因⼦,更新结束
• 更新后parent的平衡因⼦等于1或-1,更新前更新中parent的平衡因⼦变化为0->1或者0->-1,说明更新前parent⼦树两边⼀样⾼,新增的插⼊结点后,parent所在的⼦树⼀边⾼⼀边低,parent所在的⼦树符合平衡要求,但是⾼度增加了1,会影响parent的⽗亲结点的平衡因⼦,所以要继续向上更新
• 更新后parent的平衡因⼦等于2或-2,更新前更新中parent的平衡因⼦变化为1->2或者-1->-2,说明更新前parent⼦树⼀边⾼⼀边低,新增的插⼊结点在⾼的那边,parent所在的⼦树⾼的那边更⾼了,破坏了平衡,parent所在的⼦树不符合平衡要求,需要旋转处理,旋转的⽬标有两个:1.把 parent⼦树旋转平衡。2.降低parent⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不需要继续往上更新,插⼊结束

3.2右单旋

根节点的平衡因子为-2,即左子树深度过深,需要将左子树旋转,我们将根节点命名为parent,根节点的左节点命名为subL,该左节点的右节点命名为subLR,我们需要进行的操作是首先将subLR接入parent的左节点,然后判断subLR是否为空,不为空则将其父节点指针指向parent,之后就是将subL调整为调整子树的根节点即可,最后更新平衡因子

cpp 复制代码
//右单旋
void RotateR(Node* parent)
{
	//subL代表根节点的左子树,subLR则代表该左子树的右子树
	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 == nullptr)
	{
		_root = subL;
		//根节点的父亲节点为空
		subL->_parent = nullptr;
	}
	else
	{
		if (pParent->_left == parent)
		{
			pParent->_left = subL;
		}
		else
		{
			pParent->_right = subL;
		}
		//重新接入原来的节点
		subL->_parent = pParent;
	}
	//更新平衡因子,因为只改变了这两个父节点,所以只更新他们即可
	subL->_bf = 0;
	parent->_bf = 0;
}

3.3左单旋

单旋可以看做是右单旋的对称操作

cpp 复制代码
//左单旋
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 == nullptr)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (pParent->_left == parent)
		{
			pParent->_left = subR;
		}
		else
		{
			pParent->_right = subR;
		}
		subR->_parent = pParent;
	}
	subR->_bf = 0;
	parent->_bf = 0;
}

3.4左右单旋

• 场景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

cpp 复制代码
//左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

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

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

3.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

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

	RotateR(subR);
	RotateL(parent);

	int bf = subRL->_bf;

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

4.AVL树的判断

关于判断AVL树就是判断其是否每个节点平衡因子正确,并且保证每个节点的平衡因子的绝对值不能超过2,不然就不是一个AVL树,最后返回结果即可

cpp 复制代码
//判断是否为AVL树
//计算树的高度
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 _IsAVLTree(Node* root)
{
	if (root == nullptr)
	{
		return true;
	}

	int leftheight = _Heighr(root->_left);
	int rightheight = _Height(root->_right);
	int diff = rightheight - leftheight;

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

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

5.AVL树的查找

按照二叉搜索树查找即可,时间复杂度为O(logN)

cpp 复制代码
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;
}
相关推荐
XXXJessie16 分钟前
c++就业磁盘链式b树与b+树
数据结构·b树
zh路西法20 分钟前
【C++并发入门】摄像头帧率计算和多线程相机读取(上):并发基础概念和代码实现
开发语言·c++·opencv
￴ㅤ￴￴ㅤ9527超级帅32 分钟前
LeetCode hot100---双指针专题(C++语言)
c++·算法·leetcode
GarsonW41 分钟前
有关Python时间戳的计算
开发语言·python
小七蒙恩43 分钟前
go语言种的常用排序方法
算法·golang·排序算法
景天科技苑1 小时前
【Golang】Go语言中时间time相关处理方法
开发语言·后端·golang·time·go语言时间处理·go语言time
CXDNW1 小时前
【算法篇】回溯算法类(1)(笔记)
c++·笔记·算法·leetcode·回溯·递归
努力学习de小王1 小时前
QT篇:QT介绍
开发语言·qt
爱吃番茄炒蛋..1 小时前
【Windows】 C++实现 Socket 通讯
c++·windows
vir021 小时前
Excel 表格列序号
算法·leetcode·excel