C++ - AVL平衡二叉树

目录

AVL树的概念与特点

定义与特点

平衡因子与高度

AVL树的操作

AVL树的旋转

AVL树的结构(这里开始讲代码)

AVL树的插入

更新平衡因子

旋转

右单旋

左单旋

左右双旋

右左双旋

AVL树的查找

AVL的平衡检测

AVL树的删除


AVL树的概念与特点

AVL树是一种自平衡的二叉查找树,平衡二叉树且搜索二叉树

定义与特点

定义

AVL树是最先发明的自平衡二叉查找树。在AVL树中,任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树

特点

  • 它本身首先是一棵二叉搜索树。
  • 带有平衡条件:每个节点的左右子树的高度之差的绝对值(平衡因子)最多为1。
  • AVL树的基本操作一般涉及运做同在不平衡的二叉查找树所运做的同样的算法,但是要进行预先或随后做一次或多次所谓的"AVL旋转"。

平衡因子与高度

平衡因子

每个节点都有一个平衡因子,任何节点的平衡因子等于其右子树的高度减去左子树的高度,也就是说,任何节点的平衡因子等于0/1/-1。虽然AVL树并不必须要平衡因子,但是有了平衡因子可以更方便观察和控制树是否平衡。

高度要求

AVL树是高度平衡搜索二叉树,通过控制高度差去控制平衡。要求高度差不超过1,而不是高度差为0,是因为在某些情况下(如节点数为2或4时),高度差最好就是1,无法做到高度差为0。

AVL树的操作

插入操作

  • 插入一个值按二叉搜索树规则进行插入。
  • 新增节点以后,只会影响祖先节点的高度,也就是可能会影响部分祖先节点的平衡因子。所以需要更新从新增节点到根节点路径上的平衡因子。
  • 在更新平衡因子的过程中出现不平衡,需要对不平衡子树进行旋转。旋转后本质调平衡的同时,降低了子树的高度,不会再影响上一层,所以插入结束。

删除操作

  • 可以通过把要删除的节点向下旋转成一个叶子节点,接着直接剪除这个叶子节点来完成。
  • 因为在旋转成叶子节点期间最多有logn个节点被旋转,而每次AVL旋转耗费恒定的时间,所以删除处理在整体上耗费O(logn)时间。

查找操作

  • 在AVL树中查找同在一般二叉搜索树(BST)中一样,所以耗费O(logn)时间。
  • AVL树总是保持平衡的,不需要特殊的准备,树的结构不会由于查询而改变(与伸展树查找相对立,它会因为查找而变更树结构)。

AVL树的旋转

假设由于在二叉排序树上插入节点而失去平衡的最小子树根节点的指针为a(即a是离插入点最近,且平衡因子绝对值超过1的祖先节点),则失去平衡后进行的规律可归纳为下列四种情况:

  • 单向右旋平衡处理LL:由于在a的左子树根节点的左子树上插入节点,a的平衡因子由1增至2,致使以a为根的子树失去平衡,则需进行一次右旋转操作。
  • 单向左旋平衡处理RR:由于在a的右子树根节点的右子树上插入节点,a的平衡因子由-1变为-2,致使以a为根的子树失去平衡,则需进行一次左旋转操作。
  • 双向旋转(先左后右)平衡处理LR:由于在a的左子树根节点的右子树上插入节点,a的平衡因子由1增至2,致使以a为根的子树失去平衡,则需进行两次旋转(先左旋后右旋)操作。
  • 双向旋转(先右后左)平衡处理RL:由于在a的右子树根节点的左子树上插入节点,a的平衡因子由-1变为-2,致使以a为根的子树失去平衡,则需进行两次旋转(先右旋后左旋)操作。

AVL树的结构(这里开始讲代码)

首先,它的底层是搜索二叉树的基础上进行的,所以会像搜索二叉树

cpp 复制代码
struct AVLTreeNode
{
	AVLTreeNode(const T& data = T())
		: _pLeft(nullptr)
		, _pRight(nullptr)
		, _pParent(nullptr)
		, _data(data)
		, _bf(0)
	{}

	AVLTreeNode<T>* _pLeft;
	AVLTreeNode<T>* _pRight;
	AVLTreeNode<T>* _pParent;
	T _data;
	int _bf;   // 节点的平衡因子
};


// AVL: 二叉搜索树 + 平衡因子的限制
template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
    //功能...
    //我会写删除的,就在本篇一起写了
private:
	Node* _pRoot;
};

AVL树的插入

  1. 像搜索二叉树一样直接插入
  2. 新增节点后更新平衡因子
  3. 检测平衡因子,若不平衡开始旋转
  4. 根据平衡因子对不平衡子树开始向上旋转
  5. 进行左旋转、右旋转、左右旋转、右左旋转来平衡平衡因子

更新平衡因子

  • 平衡因子的值=右子树高度-左子树高度
  • 只有子树高度会影响父节点的平衡因子
  • 插入节点会新增高度,会改变插入节点的父节的平衡因子
    • 插入右节点,平衡因子++
    • 插入左节点,平衡因子- -
  • 根据父亲节点所在子树高度决定是否向上更新
    • 父亲节点平衡因子归零 不再更新 因为说明父亲节点的左右子树都平衡了 祖宗不变
    • 父亲节点变为1或-1 向上更新 父亲节点不平衡 从平衡变成不平衡 对于祖宗来说也会有不平衡
    • 父亲节点变为2或者-2 此时需要旋转
      • 当变为2/-2的时候,说明已经不符合平衡条件了(1 0 -1),说明子树的高度差已经是2了,就需要进行旋转来更新旋转因子
      • (这里到旋转的时候再分类,分了四类)左旋,右旋,左右双旋,右左双旋
    • 不断更新到平衡因子是 1 -1 的就可以时候停止
cpp 复制代码
	bool Insert(const T& data){
		if(_pRoot == nullptr)
		{
			_pRoot = new Node(data);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _pRoot;

		while (cur)
		{
			if (data > cur->_data)
			{
				parent = cur;
				cur = cur->_pRight;
			}
			else if (data < cur->_data)
			{
				parent = cur;
				cur = cur->_pLeft;
			}
			else//不支持相同 
			{
				return false;
			}
		}

		cur = new Node(data);
		if (parent->_pLeft == cur)
			parent->_pLeft = cur;
		else
			parent->_pRight = cur;
		cur->_pParent = parent;

		//这里就插进去了 然后更新平衡因子
		while (parent)
		{
			if (parent->_pLeft == cur)
				parent->_bf--;
			else
				parent->_bf++;

			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				//向上更新!
				cur = parent;
				parent = parent->_pParent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//旋转 分情况了
				break;
			}
			else
			{
				assert(false);
			}
		}
		return true;
	}

测试代码

cpp 复制代码
	AVLTree<pair<int, int>> A;
	A.Insert({ 8,1 });A.Insert({ 3,1 });A.Insert({ 10,1 });A.Insert({ 1,1 });A.Insert({ 6,1 });A.Insert({ 14,1 });

旋转

  • 旋转是难点,无论是插入还是删除的时候
  • 旋转要保证旋转完还是搜索树
  • 旋转分四类
    • 左单选/右单旋/左右双旋/右左双旋

右单旋

  • 右单旋是因为"子树集"(可以从根开始)在左边的左边插入导致左倾
  • 也就是parent的平衡因子是-1 parent的parent的平衡因子是-2
  • 右旋的方法(这里有点绕口,我用这样指代)(这里的谁的爹指向确定不因过程改变)
    • parent作为父亲 parent的parent作为祖宗
    • 祖宗的左节点指向父亲的右节点
    • 父亲的右节点指向祖宗
    • 祖宗的父亲指向父亲
    • 父亲的父亲指向之前祖宗的父亲
    • 孩子也要换爹
    • 祖宗的爹开始换节点
    • 左单旋也是这样,不过相反左单旋就不提了
  • 更新平衡因子 父亲和祖宗都是0
cpp 复制代码
	void RotateR(Node* pParent){
		//我pParent是祖宗!
		Node* subL = pParent->_pLeft;//找到父亲
		Node* subLR = subL->_pRight;//找到右孩子
		
		//祖宗的左节点指向父亲的右节点
		pParent->_pLeft = subLR;//换孩子
		if (subLR)
			subLR->_pParent = pParent;//孩子不为空给孩子换个爹

		Node* parentParent = pParent->_pParent;

		subL->_pRight = pParent;//父亲的右节点指向祖宗
		pParent->_pParent = subL;//祖宗换个爹

		//给祖宗的爹换节点
		if (parentParent == nullptr)//这里要考虑是否是根节点 换根
		{
			_pRoot = subL;
			subL->_pParent = parentParent;//空爹也是爹
		}
		else
		{
			if (pParent == pParent->_pLeft)//原来左,还是左
				parentParent->_pLeft = subL;
			else
				parentParent->_pRight = subL;

			subL->_pParent = parentParent;//给爹换个爹
		}
		//左倾旋转完后更新平衡因子 都为0 
		pParent->_bf = subL->_bf = 0;
	}

左单旋

  • 与右边单旋完全相反
  • parent的平衡因子是1,祖宗的平衡因子是2
  • 指向要反过来
cpp 复制代码
	void RotateL(Node* pParent){
		//我pParent是祖宗! X标记为更改处
		Node* subR = pParent->_pRight;//找到父亲 X
		Node* subRL = subR->_pLeft;//找到左边孩子 X
		//
		祖宗的右节点指向父亲的左节点
		pParent->_pRight = subRL;//换孩子 X
		if (subRL != nullptr)
			subRL->_pParent = pParent;//孩子不为空给孩子换个爹

		Node* parentParent = pParent->_pParent;

		subR->_pLeft = pParent;//父亲的右节点指向祖宗X
		pParent->_pParent = subR;//祖宗换个爹X

		//给祖宗的爹换节点
		if (parentParent == nullptr)//这里要考虑是否是根节点 换根
		{
			_pRoot = subR;
			subR->_pParent = parentParent;//空爹也是爹X
		}
		else
		{
			if (pParent == parentParent->_pLeft)//原来左,还是左X
				parentParent->_pLeft = subR;
			else
				parentParent->_pRight = subR;

			subR->_pParent = parentParent;//给爹换个爹
		}
		//左倾旋转完后更新平衡因子 都为0 
		pParent->_bf = subR->_bf = 0;
	}

左右双旋

左右双旋,就是先进行左旋再进行右旋

  • 这里直接调用之前的左单旋 然后再调用 右单旋就行
  • 情况:在左子树的右节点插入导致该子树左倾
  • parent的平衡因子变成了1 但是祖宗的平衡因子为-2
  • 需要先在parent进行一个左单旋,这个时候从(更新的)parent就变为-1了
  • 然后在祖宗节点就符合了右单旋的条件,右单旋就平衡了
  • 更新平衡因子(分三类)
    • 当父亲节点的左右子树高度都大于等于1的时候
      • 插入在父亲节点的右子树的左边
      • 父亲节点更新为0 祖宗为0 祖宗的右节点为1
      • 插入在父亲节点的右子树的右边
      • 父亲节点更新为-1 祖宗为0 祖宗的右节点为1
    • 当父亲节点没有孩子的时候
      • 父亲 祖宗 祖宗的右孩子都为0
cpp 复制代码
	void RotateLR(Node* pParent){ //爷爷变右父亲了
		Node* subL = pParent->_pLeft;//记住原来的父亲节点 位置不变还是父亲
		Node* subLR = subL->_pRight;//记住原来的父亲节点的有右边的孩子节点  孙子变爷爷了
		int bf = subLR->_bf;//这里用于分情况记录平衡因子
		//先以父亲节点为祖宗左旋
		RotateL(pParent->_pLeft);
		//以祖宗节点右旋
		RotateR(pParent);

		//更新平衡因子
		if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			pParent->_bf = 0;
		}
		else if (bf == -1)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			pParent->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			pParent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

右左双旋

同左右双旋类似

  • 当右父亲节点的左节点插入导致树不平衡
  • 这个时候祖宗节点平衡因子为2 父亲平衡因子为-1
  • 先让父亲节点右单旋,然后祖宗节点左单旋
  • 更新平衡因子
    • 与左右双旋相反
    • 当子树都大于等于1的时候
      • 插入左边
      • 祖宗为0 左父亲0 右父亲1
      • 插入右边
      • 祖宗为0 左父亲-1 右父亲为0
    • 当没有子树时候三个零
cpp 复制代码
	void RotateRL(Node* pParent){//爷爷变右父亲X
		Node* subR = pParent->_pRight;//记住原来的父亲节点 位置不变还是父亲
		Node* subRL = subR->_pLeft;//记住原来的父亲节点的有右边的孩子节点  孙子变爷爷了X
		int bf = subRL->_bf;//这里用于分情况记录平衡因子
		//先以父亲节点为祖宗右旋
		RotateR(pParent->_pRight);
		//以祖宗节点左旋
		RotateL(pParent);

		//更新平衡因子
		if (bf == 0)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			pParent->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			pParent->_bf = -1;
		}
		else if (bf == -1)
		{
			subR->_bf = 1;
			subRL->_bf = 0;
			pParent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

AVL树的查找

就是搜索二叉树的查找

cpp 复制代码
	Node* Find(const T& data)
	{
		Node* cur = _pRoot;
		while (cur)
		{
			if (cur->_data < data)
			{
				cur = cur->_pRight;
			}
			else if (cur->_data > data)
			{
				cur = cur->_pLeft;
			}
			else 
			{
				return cur;
			}
		}
		return nullptr;
	}

AVL的平衡检测

利用高度差检测

cpp 复制代码
	bool _IsAVLTree(Node* pRoot)
	{
		if (pRoot == nullptr)
			return true;
		int LHigh = _Height(pRoot->_pLeft);
		int RHigh = _Height(pRoot->_pRight);
		int D = RHigh - LHigh;
		if (D < -1 || D > 1)
		{
			cout << pRoot->_data.first <<"高度异常" << endl;
			return false;
		}
		
		if (pRoot->_bf != D)
		{
			cout << pRoot->_data.first << "平衡因子异常" << endl;
		}

		return _IsAVLTree(pRoot->_pLeft) && _IsAVLTree(pRoot->_pRight);
	}
	size_t _Height(Node* pRoot){
		if (pRoot == nullptr)
			return 0;
		int LHigh = _Height(pRoot->_pLeft);
		int RHigh = _Height(pRoot->_pRight);

		return LHigh > RHigh ? LHigh + 1 : RHigh + 1;
	}

AVL树的删除

AVL树的删除和插入一样复杂,下篇目分开讲解了

相关推荐
2301_8074492012 分钟前
字符串相乘——力扣
java·算法·leetcode
shylyly_34 分钟前
list的模拟实现
数据结构·c++·链表·迭代器·list·list的模拟实现
ianozo1 小时前
数据结构--【栈与队列】笔记
数据结构·笔记
---yx8989781 小时前
数字人系统源码---v10技术五大底层架构链路全局开发思路
算法·架构·数字人·数字人源码·数字人系统
xiao--xin1 小时前
LeetCode100之二叉搜索树中第K小的元素(230)--Java
java·算法·leetcode·二叉树·树的统一迭代法
zhengtianzuo1 小时前
006-获取硬件序列号
c++
路飞雪吖~1 小时前
数据结构 && 常见的排序算法
数据结构·算法·排序算法
手握风云-1 小时前
Java数据结构第二十一期:解构排序算法的艺术与科学(三)
数据结构·算法·排序算法
爱吃柠檬呀2 小时前
《C陷阱与缺陷》读书笔记(一)
c语言·开发语言·算法·《c陷阱与缺陷》·编写程序
壮志凌云2 小时前
配对样本t检验
算法