【数据结构】AVL树

1、常见搜索方式总结

1、暴力搜索

2、二分搜索(问题:有序,伴随着插入删除,维护成本很高)

3、二叉搜索树(问题:极端场景退化为类似链表结构)

4、二叉平衡搜索树 AVL树/红黑树
5、多叉平衡搜索树
6、哈希表

2 AVL 树

2.1 AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序,二叉搜索树将退化为单支树,查
找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii
和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而提高搜索效率。

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

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

平衡因子不恒为 0 的原因是有时候无法做到平衡因子为 0,比如二叉树有 4 个结点时。

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

2.2 AVL树节点的定义

template<class T>
struct AVLTreeNode
{
     AVLTreeNode(const T& data)
         : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
         , _data(data), _bf(0)
     {}
     AVLTreeNode<T>* _pLeft;   // 该节点的左孩子
     AVLTreeNode<T>* _pRight;  // 该节点的右孩子
     AVLTreeNode<T>* _pParent; // 该节点的父结点
     T _data;
     int _bf;                  // 该节点的平衡因子
};

与普通搜索二叉树多了指向父结点的指针和平衡因子,存储指向父结点的指针是为了在调整平衡因子时可以快速找到父结点,但使得旋转操作变得更加复杂。也可以不使用平衡因子来实现AVL树,只是使用平衡因子会更加直观。

平衡因子(balance factor)= 右子树的高度 - 左子树的高度

2.3 AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子(balance factor),因此AVL树也可以看成是二叉搜索树。那么
AVL树的插入过程可以分为两步:

1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子

调整节点的平衡因子的策略:

pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent 的平衡因子分可以是:-1,0, 1, 插入后分以下两种情况:
  1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
  2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可
  
 此时:插入 pCur 后,pParent的平衡因子可能有三种情况:0,正负1, 正负2
  1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整
成0,此时以pParent为根的树的高度没有增加,不需要继续向上更新
  2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更
新成正负1,此时以pParent为根的树的高度增加,需要继续向上更新
  3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理

bool Insert(const T& data)
{
    // 1. 先按照二叉搜索树的规则将节点插入到AVL树中
    // ...
    
    // 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否
    // 破坏了AVL树的平衡性
    while (pParent) // 最坏的情况下,需要更新到根结点
    {
        // 更新pParent的平衡因子
        if (pCur == pParent->_pLeft)
            pParent->_bf--;
        else
            pParent->_bf++;
        // 更新后检测pParent的平衡因子
        if (0 == pParent->_bf)
        {
            break;
        }
        else if (1 == pParent->_bf || -1 == pParent->_bf)
        {
            // 插入前pParent的平衡因子是0,插入后双亲的平衡因为为1 或者 -1
            // 说明以pParent为根的二叉树的高度增加了一层,因此需要继续向上调整
            pCur = pParent;
            pParent = pCur->_pParent;
        }
        else
        {
            // 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
            // 为根的树进行旋转处理
            if (2 == pParent->_bf)
            {
                // ...
            }
            else // -2
            {
                // ...
            }
        }
    }
    return true;
}

2.4 AVL树的旋转

由于能够触发旋转操作的树的形状的数量是无穷无尽的,而旋转的过程是一样的,所以下面的示例图中,没有完整的画出一颗AVL树的全貌,而是用违反AVL树的规则的那个结点作为根(它可能是左子树或者右子树的根),以一个竖着的矩形表示子树,矩形内是子树的名称,矩形外字母表示子树的高度。并且假设每一个竖着的矩形表示的子树都是符合AVL规则的子树,现在只考虑插入后一定会触发旋转的情况(插入后不触发旋转的情况就是上文所描述的情况)

1. 新节点插入较高左子树的左子树:右单旋(父结点 bf == -2,左子树 bf == -1)

1、把60的左孩子改为 b

2、如果b存在,把 b 的父指针指向60

3、把 60 作为 30 的右孩子

4、修改 60 的父指针之前,把 60 的父指针存储起来

5、把 60 的父指针指向 30

6、30 的父指针指向第 4 步存储的父指针指向的结点

7、如果60是根节点,更新根节点的指针指向30;如果60是左子树,把60的父结点的左指针指向30,如果60是右子树,60的父结点的右指针指向30

8、更新平衡因子

/*
  上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左
子树增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,
右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,
而如果30有右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,
更新节点的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树
     如果是根节点,旋转完成后,要更新根节点
     如果是子树,可能是某个节点的左子树,也可能是右子树
*/
void _RotateR(PNode pParent)
{
    // pSubL: pParent的左孩子
    // pSubLR: pParent左孩子的右孩子
     PNode pSubL = pParent->_pLeft;
     PNode pSubLR = pSubL->_pRight;
    
    // 旋转完成之后,30的右孩子作为双亲的左孩子
     pParent->_pLeft = pSubLR;
    
    // 如果30的左孩子的右孩子存在,更新双亲
     if(pSubLR)
     pSubLR->_pParent = pParent;
    
    // 60 作为 30的右孩子
    pSubL->_pRight = pParent;
    
    // 因为60可能是棵子树,因此在更新其双亲前必须先保存60的双亲
     PNode pPParent = pParent->_pParent;
    
    // 更新60的双亲
     pParent->_pParent = pSubL;
    
    // 更新30的双亲
     pSubL->_pParent = pPParent;
    
    // 如果60是根节点,根新指向根节点的指针
     if(NULL == pPParent)
     { 
        _pRoot = pSubL;
        pSubL->_pParent = NULL; 
     }
     else
     {
         // 如果60是子树,可能是其双亲的左子树,也可能是右子树
         if(pPParent->_pLeft == pParent) pPParent->_pLeft = pSubL;
         else pPParent->_pRight = pSubL;
     }
    
    // 根据调整后的结构更新部分节点的平衡因子
     pParent->_bf = pSubL->_bf = 0;
}

2. 新节点插入较高右子树的右子树:左单旋:(父结点 bf == 2,左子树 bf == 1)参考左单旋

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

(父结点 bf == -2,左子树 bf == 1)

将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再
考虑平衡因子的更新。由于插入位置可以是较高左子树的右子树的左子树,或者是较高左子树的右子树的右子树,虽然旋转的过程是一样的,但旋转的结果不同,需要根据插入位置来更新平衡因子,即要根据较高左子树的右子树的平衡因子来更新平衡因子。由于上图具有一般性,可以根据上图的结果直接得出旋转后各结点的平衡因子(右左单旋一样)。还有一种情况需要考虑:如果较高左子树的右子树的根本身就是插入的结点

void _RotateLR(PNode pParent)
{
     PNode pSubL = pParent->_pLeft;
     PNode pSubLR = pSubL->_pRight;
    
    // 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子
    // 来调整其他节点的平衡因子
     int bf = pSubLR->_bf;
    
    // 先对30进行左单旋
     _RotateL(pParent->_pLeft);
    
    // 再对90进行右单旋
     _RotateR(pParent);
 
    // 调整其他节点的平衡因子

    // 新结点插入在 pSubLR 的左边
    if(1 == bf) pSubL->_bf = -1; 

    // 新结点插入在 pSubLR 的右边
    else if(-1 == bf) pParent->_bf = 1;

    // pSubLR 自己就是新插入的结点
    // else if(0 == bf) ...
    // 不需要修改了, _RotateL和_RotateR 已经帮忙修改了
}

4. 新节点插入较高右子树的左侧---右左:先右单旋再左单旋 (参考右左双旋)

总结:

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑

  1. 1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
  • 当pSubR的平衡因子为1时,执行左单旋
  • 当pSubR的平衡因子为-1时,执行右左双旋
  1. 2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
  • 当pSubL的平衡因子为-1时,执行右单旋
  • 当pSubL的平衡因子为1时,执行左右双旋

旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

2.5 AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这
样可以保证查询时高效的时间复杂度,即 logN。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

3、模拟实现

#pragma once
#include <vector>
#include <iostream>
#include <assert.h>
using namespace std;
template<class T>
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:
	AVLTree()
		: _pRoot(nullptr)
	{}

	// 在AVL树中插入值为data的节点
	bool Insert(const T& data)
	{
		if (_pRoot == nullptr)
		{
			_pRoot = new Node(data);
			return true;
		}
		
		Node* cur = _pRoot;
		Node* pParent = _pRoot;

		while (cur)
		{
			if (data < cur->_data)
			{
				pParent = cur;
				cur = cur->_pLeft;
			}
			else if (data > cur->_data)
			{
				pParent = cur;
				cur = cur->_pRight;
			}
			else if (data == cur->_data)
			{
				return false;
			}
		}

		if (data < pParent->_data)
		{
			cur = new Node(data);
			pParent->_pLeft = cur;
		}
		else if (data > pParent->_data)
		{
			cur = new Node(data);
			pParent->_pRight = cur;
		}

		cur->_pParent = pParent;

		while (pParent)
		{
			if (cur == pParent->_pLeft)
			{
				pParent->_bf--;
			}
			else if (cur == pParent->_pRight)
			{
				pParent->_bf++;
			}

			if (pParent->_bf == 0)
			{
				break;
			}
			else if (pParent->_bf == 2 || pParent->_bf == -2)
			{
				if (pParent->_bf == -2 && cur->_bf == -1) RotateR(pParent);
				else if (pParent->_bf == 2 && cur->_bf == 1) RotateL(pParent);
				else if (pParent->_bf = -2 && cur->_bf == 1) RotateLR(pParent);
				else if (pParent->_bf = 2 && cur->_bf == -1) RotateRL(pParent);

				break;
			}
			else if (pParent->_bf == -1 || pParent->_bf == 1)
			{
				cur = cur->_pParent;
				pParent = pParent->_pParent;
			}
			else assert(false);
		}
			
		return true;
	}

	// AVL树的验证
	bool IsAVLTree()
	{
		return _IsAVLTree(_pRoot);
	}

	void inorder_traversal()
	{
		_inorder_traversal(_pRoot);
	}

	void _inorder_traversal(Node* _pRoot)
	{
		if (_pRoot == nullptr) return;

		_inorder_traversal(_pRoot->_pLeft);

		cout << _pRoot->_data << " ";

		_inorder_traversal(_pRoot->_pRight);
	}

private:
	// 根据AVL树的概念验证_pRoot是否为有效的AVL树
	bool _IsAVLTree(Node* _pRoot)
	{
		if (_pRoot == nullptr) return true;

		int bf = _Height(_pRoot->_pRight) - _Height(_pRoot->_pLeft);
		if (bf != _pRoot->_bf || bf > 1 || bf < -1) return false;

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

		int hl = _Height(_pRoot->_pLeft);
		int hr = _Height(_pRoot->_pRight);

		return (hl > hr ? hl : hr) + 1;
	}
	// 右单旋
	void RotateR(Node* pParent)
	{
		Node* pLeft = pParent->_pLeft;

		pParent->_pLeft = pLeft->_pRight;
		if (pLeft->_pRight) pLeft->_pRight->_pParent = pParent;

		pLeft->_pRight = pParent;

		Node* pPParent = pParent->_pParent;

		if (pPParent)
		{
			if (pPParent->_pLeft == pParent)
			{
				pPParent->_pLeft = pLeft;
			}
			else if (pPParent->_pRight == pParent)
			{
				pPParent->_pRight = pLeft;
			}
		}
		else
		{
			_pRoot = pLeft;
		}

		pLeft->_pParent = pPParent;
		pParent->_pParent = pLeft;

		pParent->_bf = pLeft->_bf = 0;
	}
	// 左单旋
	void RotateL(Node* pParent)
	{
		Node* pRight = pParent->_pRight;

		pParent->_pRight = pRight->_pLeft;
		if (pRight->_pLeft) pRight->_pLeft->_pParent = pParent;

		pRight->_pLeft = pParent;

		Node* pPParent = pParent->_pParent;

		if (pPParent)
		{
			if (pPParent->_pLeft == pParent)
			{
				pPParent->_pLeft = pRight;
			}
			else if (pPParent->_pRight == pParent)
			{
				pPParent->_pRight = pRight;
			}
		}
		else
		{
			_pRoot = pRight;
		}

		pRight->_pParent = pPParent;
		pParent->_pParent = pRight;

		pParent->_bf = pRight->_bf = 0;
	}
	// 右左双旋
	void RotateRL(Node* pParent)
	{
		Node* pRight = pParent->_pRight;
		Node* pRightL = pRight->_pLeft;
		int bf = pRightL->_bf;

		RotateR(pRight);
		RotateL(pParent);

		if (bf == 1) pParent->_bf = -1;
		else if (bf == -1) pRight->_bf = 1;
	}
	// 左右双旋
	void RotateLR(Node* pParent)
	{
		Node* pLeft = pParent->_pLeft;
		Node* pLeftR = pLeft->_pRight;
		int bf = pLeftR->_bf;

		RotateL(pLeft);
		RotateR(pParent);

		if (bf == -1) pParent->_bf = 1;
		else if (bf == 1) pLeft->_bf = -1;
	}

private:
	Node* _pRoot;
};

4、调试 AVL 树的技巧

可以写一个函数来帮助我们快速判断一个 AVL 树是否是合格的 AVL 树,这个函数要判断树的高度是否平衡、平衡因子是否正确。不能仅通过结点内存储的平衡因子来判断是否平衡,要实实在在的测定树的高度。

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

1. 验证其为二叉搜索树

  • 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树

2. 验证其为平衡树

  • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
  • 节点的平衡因子是否计算正确
int _Height(PNode pRoot);
bool _IsBalanceTree(PNode pRoot)
{
    // 空树也是AVL树
    if (nullptr == pRoot) return true;

    // 计算pRoot节点的平衡因子:即pRoot左右子树的高度差
    int leftHeight = _Height(pRoot->_pLeft);
    int rightHeight = _Height(pRoot->_pRight);
    int diff = rightHeight - leftHeight;
    
    // 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
    // pRoot平衡因子的绝对值超过1,则一定不是AVL树
    if (diff != pRoot->_bf || (diff > 1 || diff < -1))
        return false;
    
    // pRoot的左和右如果都是AVL树,则该树一定是AVL树
    return _IsBalanceTree(pRoot->_pLeft) && _IsBalanceTree(pRoot->_pRight);
}

可以写个 if 语句来充当条件断点,比如我们知道当插入 m 时,AVL 树出现问题:

if(要插入的值 == m)
{
    int x = 0; // 由于空语句不会触发断点,这里随便写句代码并给它打上断点
}

验证用例

常规场景1

  • {16, 3, 7, 11, 9, 26, 18, 14, 15}

特殊场景2

  • {4, 2, 6, 1, 3, 5, 15, 7, 16, 14}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值