AVL树的实现

AVL树的概念

AVL树是最先发明的平衡二叉树,她是一棵空树或者具备以下性质的二叉搜索树:

  • 左右子树都是二叉搜索树
  • 左右子树的高度差的绝对值不超过1

AVL树是一棵高度平衡的二叉搜索树通过控制高度差去控制平衡。

在AVL树中我们需要引入应该概念:平衡因子。每个平衡因子等同于右子树的高度减去左子树的高度,也就是任何结点的平衡因子等于0/1/-1。

那为什么不控制高度差为0呢?这样二叉树不是更平衡吗?答案很简单,做不到,假设一个数为两个结点或者为4个结点,此时最好的平衡状态就是高度差为1。

AVL树的实现:

cpp 复制代码
template<class K, class V>
struct AVLTreeNode
{
	std::pair<k, V> _kv;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf;
	AVLTreeNode(const std::pair<K,V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_bf(0)
	{}
};

AVL树的插入

AVL树的插入大概遵循以下过程:

  • 按照二叉搜索树的规则进行插入数值
  • 新增结点之后,会影响祖先结点的高度,也就是可能影响部分祖先结点的平衡因子,所以更新从新增结点->根节点路劲上的平衡因子,最坏情况要更新到根节点,有些情况更新到中间结点即可。
  • 更新平衡因子过程中没有出现问题,插入结束。
  • 更新过程中出现不平衡,对不平衡的子树旋转,旋转后调平衡的同时,本质上就是降低了子树的高度,不会影响上一层,插入结束。
平衡因子更新

平衡因子 = 右子树高度 - 左子树高度

只有子树高度变化才会影响当前结点平衡因子。插入节点会增加高度,所以新增节点在parent的右子树时,parent的平衡因子++,在左子树时,parent的平衡因子--。parent所在子树的高度是否变化决定了是否会继续向上更新。
更新停止条件:

  • 更新后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⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不需要继续往上更新,插⼊结束
  • 不断更新,更新到根,跟的平衡因⼦是1或 - 1也停⽌了。
cpp 复制代码
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
	bool Insert(const std::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)
			{
				//不平衡,旋转处理
				if(parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if(parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
				}
				else if(parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				else if(parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				break;
			}
			else assert(false);
		}
		return true;
	}
private:
    Node* _root = nullptr;
};

旋转

旋转的原则:
  • 保持树的规则
  • 让旋转的树从不满足变平衡,其次降低旋转树的高度

旋转总共分为四种,右单旋 / 左单旋 / 左右双旋 / 右左双旋。

右单旋

上图展示的是以10为根的树,有a / b / c抽象为三棵⾼度为h的⼦树(h >= 0),a / b / c均符合AVL树的要求(10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根)。这里a / b / c是三颗高度为h的子树,是一种抽象的概括表示,代表了所有右单旋的场景,由于情况是在非常多,这里我们不一一列举了。(5和10可更改为任意数,只要保证满足AVL树规则即可)
旋转核心步骤:因为 5 < b子树的值 < 10,将b变成10的左子树,10变成5的右子树,5变成这颗树新的根,符合搜索树的规则,控制了平衡,同时这棵树的高度恢复到了插入之前的h+2,符合旋转原则。如果插入之前10整棵树的一个局部子树,旋转后不会再影响上一层,插入就结束了。

cpp 复制代码
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    void  RotateR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;
        
        //需要注意除了要修改孩⼦指针指向,还是修改⽗亲
        parent->_left = subLR;
        if (subLR)
            subLR->_parent = parent;
        Node* parentParent = parent->_parent;
        subL->_right = parent;
        parent->_parent = subL;
        // parent有可能是整棵树的根,也可能是局部的⼦树

        //如果是整棵树的根,要修改_root
        //如果是局部的指针要跟上⼀层链接
        if (parentParent == nullptr)
        {
            _root = subL;
            subL->_parent = nullptr;
        }
        else
        {
            if (parent == parentParent->_left)
            {
                parentParent->_left = subL;
            }
            else
            {
                parentParent->_right = subL;
            }
            subL->_parent = parentParent;
        }
        parent->_bf = subL->_bf = 0;
    }
private:
    Node* _root = nullptr;
};
左单旋

上图展示的依然是以10为根的树,有a / b / c抽象为三颗高度为h的子树,a / b / c均符合AVL树的要求,代表了所有左单旋的场景,实际左单旋形态有很多种,具体和上面右旋类似。

在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h + 1,不断向上更新平衡因⼦,导致10的平衡因⼦从1变成2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树右边太⾼了,需要往左边旋转,控制两棵树的平衡。
旋转核心步骤:因为10 < b⼦树的值 < 15,将b变成10的右⼦树,10变成15的左⼦树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h + 2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

cpp 复制代码
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    void RotateL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        parent->_right = subRL;
        if (subRL)
            subRL->_parent = parent;
        Node* parentParent = parent->_parent;
        subR->_left = parent;
        parent->_parent = subR;

        if (parentParent == nullptr)
        {
            _root = subR;
            subR->_parent = nullptr;
        }
        else
        {
            if (parent == parentParent->_left)
            {
                parentParent->_left = subR;
            }
            else
            {
                parentParent->_right = subR;
            }
            subR->_parent = parentParent;
        }
        parent->_bf = subR->_bf = 0;
    }
private:
    Node* _root = nullptr;
};
左右双旋

从上图可以看出,如果左边高时,插入位置不在a子树,而是插在b子树,b的高度从h变为h+1旋转,但右旋无法解决问题,右旋后,我们树依然不平衡。右单旋解决的纯粹的左边⾼,但是插⼊在b⼦树中,10为跟的⼦树不再是单纯的左边⾼,对于10是左边⾼,但是对于5是右边⾼,需要⽤两次旋转才能解决,以5为旋转点进⾏⼀个左单旋,以10为旋转点进⾏⼀个右单旋,这棵树这棵树就平衡了。

下⾯我们将a / b / c⼦树抽象为⾼度h的AVL⼦树进⾏分析,另外我们需要把b⼦树的细节进⼀步展开为8和左⼦树⾼度为h - 1的e和f⼦树,因为我们要对b的⽗亲5为旋转点进⾏左单旋,左单旋需要动b树中的左⼦树。b⼦树中新增结点的位置不同,平衡因⼦更新的细节也不同,通过观察8的平衡因⼦不同,这⾥我们要分三个场景讨论

  • 场景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 复制代码
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    void RotateLR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;
        int bf = subLR->_bf;
        RotateL(parent->_left);
        RotateR(parent);
        if (bf == 0)
        {
            subL->_bf = 0;
            subLR->_bf = 0;
            parent->_bf = 0;
        }
        else if (bf == -1)
        {
            subL->_bf = 0;
            subLR->_bf = 0;
            parent->_bf = 1;
        }
        else if (bf == 1)
        {
            subL->_bf = -1;
            subLR->_bf = 0;
            parent->_bf = 0;
        }
        else
        {
            assert(false);
        }
    }
private:
    Node* _root = nullptr;
};
右左双旋

跟左右双旋类似,下⾯我们依然将a / b / c⼦树抽象为⾼度h的AVL⼦树进⾏分析。(因为情况和上面差距不是特别大,因此笔者这里偷个小懒,就不将完整的变换图全画出来了)我们依然需要分为三个场景进行讨论

  • 场景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 复制代码
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    void RotateRL(Node* parent)
    {
        Node* subR = parent->_right;
        Node* subRL = subR->_left;
        int bf = subRL->_bf;
        RotateR(parent->_right);
        RotateL(parent);
        if (bf == 0)
        {
            subR->_bf = 0;
            subRL->_bf = 0;
            parent->_bf = 0;
        }
        else if (bf == 1)
        {
            subR->_bf = 0;
            subRL->_bf = 0;
            parent->_bf = -1;
        }
        else if (bf == -1)
        {
            subR->_bf = 1;
            subRL->_bf = 0;
            parent->_bf = 0;
        }
        else
        {
            assert(false);
        }
    }
private:
    Node* _root = nullptr;
};

AVL树的查找

AVL树的查找和二叉搜索树是一样的,都遵循相同的规则,因此笔者这里不再过多赘述,有疑问的可以移步二叉搜索树简述。查找效率为logN.

cpp 复制代码
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    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;
    }
private:
    Node* _root = nullptr;
};

AVL树的平衡检查

对AVL树平衡检查只需要检查每个结点的高度差也就是检查平衡因子即可。

cpp 复制代码
template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    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 _IsBalanceTree(Node* root)
    {
        //空树也是AVL树
        if (nullptr == root)
            return true;
        //计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差
        int leftHeight = _Height(root->_left);
        int rightHeight = _Height(root->_right);
        int diff = rightHeight - leftHeight;
        //如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者
        // pRoot平衡因⼦的绝对值超过1,则⼀定不是AVL树
        if (abs(diff) >= 2)
        {
            cout << root->_kv.first << "⾼度差异常" << endl;
            return false;
        }
        if (root->_bf != diff)
        {
            cout << root->_kv.first << "平衡因⼦异常" << endl;
            return false;
        }
        // pRoot的左和右如果都是AVL树,则该树⼀定是AVL树
        return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
    }

private:
    Node* _root = nullptr;
};

谢谢阅读!

相关推荐
小罗和阿泽18 小时前
java 【多线程基础 三】
java·开发语言
想你依然心痛18 小时前
从x86到ARM的HPC之旅:鲲鹏开发工具链(编译器+数学库+MPI)上手与实战
java·开发语言·arm开发·鲲鹏·昇腾
黎雁·泠崖18 小时前
二叉树知识体系全梳理:从基础到进阶一站式通关
c语言·数据结构·leetcode
山上三树18 小时前
详细介绍 C/C++ 中的内存泄漏
c语言·c++
967718 小时前
python基础自学
开发语言·windows·python
毕设源码-朱学姐18 小时前
【开题答辩全过程】以 基于Python的茶语店饮品管理系统的设计与实现为例,包含答辩的问题和答案
开发语言·python
Legendary_00818 小时前
LDR6020:单C口可充可放电PD协议芯片,开启USB2.0数据传输新体验
c语言·开发语言
蜕变菜鸟18 小时前
JS的Object.keys()和sort()排序的用法
数据结构·算法
源代码•宸18 小时前
Golang基础语法(go语言error、go语言defer、go语言异常捕获、依赖管理、Go Modules命令)
开发语言·数据库·后端·算法·golang·defer·recover