【数据结构进阶】AVL树(详解)

目录

一、AVL树的概念

二、AVL树的实现

1.节点的定义

2.AVL树的插入

2.1平衡因子的更新

2.2旋转

1.右单旋

2.左右双旋

3.节点的查找

4.节点个数大小

三、总结


一、AVL树的概念

二叉搜索树如果插入有序或接近有序的数值会退化成单支树,查找效率很低。前苏联科学家G. M. Adelson-Velsky和E. M. Landis共同发表论文,提出 AVL树 ,是一种**⾃** 平衡二叉查找树 ,能够在插入或删除后自动调整,保持平衡。
一棵AVL树或是空树,或是有以下特点:

1.它左右 子树的高度差不超过1。
2.它的 左右子树也是都是AVL。

AVL树实现这⾥我们引⼊⼀个 平衡因⼦(balance factor) 的概念,每个结点都有⼀个平衡因⼦,这里我们采用主流教材的定义公式: 任何结点的平衡因子等于右子树的高度减去左子树的高度 ,也就是说任何结点的平衡因⼦等于0/1/-1

二、AVL树的实现

1.节点的定义

为了方便后续的操作,这里将AVL树中的结点定义为三叉链结构 ,并在每个结点当中引入平衡因子(右子树高度-左子树高度)。

cpp 复制代码
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) 
    {}
};

++实现AVL树不一定要用平衡因子,也有其它方法,但相比之下使用平衡因子实现更简单。++

2.AVL树的插入

AVL树插入的总过程是先按照二叉搜索树的规则去插入(小于往左走,大于往右走,走到空插入),然后根据插入的位置更新父亲及祖先的平衡因子,更新过程中如果平衡因子不合法 就会进行旋转来调整树的结构。

2.1平衡因子的更新

设cur是新增节点,parent是新结点的父亲节点。

1.按照平衡因子的值为右子树的高度减去左子树的高度,所以:

  • 当 cur 是 parent 的左孩子,则 parent 的平衡因子 - 1;
  • 当 cur 是 parent 的右孩子,则 parent 的平衡因子 + 1。

2.parent的平衡因子更新后,以parent为根的树的高度可能会改变,这时要根据bf的值来判断是否继续向祖先方向更新平衡因子。

• parent的平衡因子为0 (则更新之前平衡因子为-1/1),以parent为根的树的高度不变,无需向上更新。

• parent的平衡因子为1/-1(则更新之前平衡因子为0),以parent为根的树的高度 + 1,需要向上更新,直到某个节点平衡因子为0为止

• parent的平衡因子为2/-2 (则更新之前平衡因子为1/-1),此时以parent为根的树已经不平衡,需要进行旋转。

停止更新的条件是:更新完根节点或某个节点平衡因子更新后的值为0。

节点插入和更新平衡因子的代码:

cpp 复制代码
//插入
bool Insert(const pair<K, V>& kv)
{
	//根节点为空,直接插入
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_size++;
		return true;
	}
 
	Node* parent = nullptr;
	Node* cur = _root;
	//寻找合适位置
	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指针指向父亲
	cur->_parent = parent;
 
	//更新平衡因子
	while (parent)
	{
		//根据插入节点的位置更新父亲的平衡因子
		if (cur == parent->_left)
		{
			parent->_bf--;
		}
		else
		{
			parent->_bf++;
		}
 
		//判断平衡因子的值
		if (parent->_bf == 0)//等于0,停止更新
		{
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)//等于1,继续向上更新
		{
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)//等于2,需要旋转
		{
			//开始旋转
            //
 
			break;
		}
		else //不可能有其他情况,走到这里直接断言报错
		{
			assert(false);
		}
	}
	_size++;
	return true;
}

2.2旋转

当平衡因子是2/-2时,进行旋转。

旋转分为4种,**左单旋,右单旋,左右双旋,右左双旋,**下面实现右单旋和左右双旋,剩下两个实现过程和他们相同。

1.右单旋

新增节点在较高子树的左侧时,进行右单旋。(根右边低,带着根往下移)

旋转过程:将节点10标记为parent,5为subL,b为subLR,将parent的左指针指向subLR,subL的右指针指向parent,subL成为新的根。旋转完成后,subL和parent的平衡因子都置为0。

实现代码:

cpp 复制代码
//右单旋
void RotateR(Node* parent)
{
	//先标记subL和subLR
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
 
	parent->_left = subLR;//让parent的左指针指向subLR
 
	if (subLR) subLR->_parent = parent;//让subLR的父指针指向parent。注意subLR可能为空,需要判断
 
	//标记parent的父亲
	Node* ppNode = parent->_parent;
 
	subL->_right = parent;//让subL的右指针指向parent
	parent->_parent = subL;//parent的父指针指向subL
 
	//处理ppNode和subL的关系
	if (ppNode == nullptr)//ppNode为空说明parent之前是根节点
	{
		_root = subL;//subL成为新根
		subL->_parent = nullptr;
	}
	else
	{
		if (ppNode->_left == parent) ppNode->_left = subL;
		else ppNode->_right = subL;
		subL->_parent = ppNode;
	}
 
	//调整平衡因子
	parent->_bf = 0;
	subL->_bf = 0;
}
2.左右双旋

如图,若是插入在较高子树的右边,只进行单旋不能调节好树的状态。

这时就要进行双旋,双旋可以套用单旋,但注意要调节平衡因子。

1)插入新结点

2)以10为根节点进行左单旋

3)以20为根节点进行右单旋

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

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

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

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

代码实现:

cpp 复制代码
//左右双旋
void RotateLR(Node* parent)
{
	//先标记subL和subLR
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
 
	//记录subLR的平衡因子
	int bf = subLR->_bf;
 
	RotateL(subL);//将subL作为旋转点,进行一次左单旋
	RotateR(parent);//将parent作为旋转点,进行一次右单旋
 
	//调整平衡因子
	if (bf == 0)//场景1
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)//场景2
	{
		subL->_bf = 0;
		subLR->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)//场景3
	{
		subL->_bf = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else //不可能有其他情况,走到这里直接断言报错
	{
		assert(false);
	}
}

2.3.节点的查找

与传统二叉搜索树的查找相同

cpp 复制代码
//查找结点
Node* Find(const K&key)//查找key
{
	Node* cur = _root;
 
	while (cur)
	{
		if (cur->_kv.first > key)//key在左子树
		{
			cur = cur->_left;
		}
		else if (cur->_kv.first < key)//key在右子树
		{
			cur = cur->_right;
		}
 
		else
		{
			return cur;//相等,返回当前结点
		}
	}
 
	//没有找到返回空
	return nullptr;
}

2.4.节点个数大小

cpp 复制代码
public:
	int size()
	{
		return _size(_root);
	}
private:
	Node* _root=nullptr;
	int _size(Node* root)
	{
		if (root == nullptr)
			return 0;
 
		return _size(root->_left) + _size(root->_right) + 1;
	}

三、总结

AVL树是绝对平衡的二叉搜索树,每个节点子树高度差不超过1,查询效率高log2N。其中旋转是重点,可以重点看一下。感谢观看,如有错误欢迎指出。

相关推荐
放荡不羁的野指针2 小时前
leetcode150题-双指针
数据结构·算法·leetcode
面条有点辣2 小时前
C++内存管理基础概念入门到理解
c++
蒹葭玉树2 小时前
【C++上岸】C++常见面试题目--网络篇(第二十六期)
网络·c++·面试
老四啊laosi2 小时前
[C++初阶] 10. string模拟实现
c++·stl·string
好学且牛逼的马2 小时前
【Hot100|15-LeetCode 238. 除自身以外数组的乘积】
数据结构·算法·leetcode
EmbedLinX2 小时前
C++ STL 学习笔记
c++·stl
m0_736919102 小时前
C++中的策略模式实战
开发语言·c++·算法
孞㐑¥2 小时前
算法—位运算
c++·经验分享·笔记·算法
从此不归路2 小时前
Qt5 进阶【9】模型-视图框架实战:从 TableView 到自定义模型的一整套落地方案
开发语言·c++·qt