平衡二叉搜索树(AVL的理论剖析+代码实现)

平衡二叉搜索树(AVL树)


1.使用AVL树的原因

  1. BST树的插入与删除操作都必须先查找,查找效率代表了BST树中各个操作的效率
  2. 二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,[时间复杂度]为O(N)
  3. 因此 map、set 等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡二叉搜索树(AVL树)来实现

请添加图片描述

最优情况:有n个结点的BST树为完全二叉树,查找效率为O(log2N)

最差情况:有n个结点的BST树退化为单支树,查找效率为O(N)


2.AVL树的基本概念

平衡二叉搜索树(Self-balancing binary search tree),又称AVL树

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

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

  1. 每个节点的左右子树高度之差(简称平衡因子 Balance Factor)的绝对值不超过 1 (-1/0/1)
    • 平衡因子(用来判断是否需要进行平衡操作) = 右子树的高度 - 左子树的高度
  2. 每一个子树都是平衡二叉搜索树
    • 请添加图片描述

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

思考:为什么左右子树高度差不规定成0呢?
因为在2、4等结点数的情况下,不可能做到左右高度相等


3.AVL树的结点定义

AVL树节点是一个三叉链结构,除了指向左右孩子的指针,还有一个指向其父亲的指针,数据域是键值对,即pair对象,还引入了平衡因子,用来判断是否需要进行平衡操作

// AVL树节点的定义(KV模型)
template<class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;  // 键值对
	int _bf;         // 平衡因子(balance factor) = 右子树高度 - 左子树高度
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent; // 双亲指针

	// 构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		,_bf(0)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
	{}
};

// AVL树的定义(KV模型)
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;

private:
	Node* _root;

public:
	// 成员函数
}

4.AVL树插入结点操作

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

  1. 插入新结点
  2. 更新树的平衡因子
  3. 根据更新后树的平衡因子的情况,来控制树的平衡(旋转操作)

1.插入新结点

和二叉搜索树插入方式一样,先查找,再插入

// 插入节点
bool AVLTree::Insert(const pair<K, V>& kv)
{
    // 如果树为空,则直接插入节点
    if (_root == nullptr)
    {
        _root = new Node(kv);
        return true;
    }

    // 如果树不为空,找到适合插入节点的空位置
    Node* parent = nullptr;  // 记录当前节点的父亲
    Node* cur = _root;       // 记录当前节点
    while (cur)
    {
        if(kv.first > cur->_kv.first) // 插入节点键值k大于当前节点
        {
            parent = cur;
            cur = cur->_right;
        }
        else if(kv.first < cur->_kv.first) // 插入节点键值k小于当前节点
        { 
            parent = cur;
            cur = cur->_left;
        }
        else // 插入节点键值k等于当前节点
        {
            return false;
        }
    }
    // while循环结束,说明找到适合插入节点的空位置了

    // 插入新节点
    cur = new Node(kv); // 申请新节点
    // 判断当前节点是父亲的左孩子还是右孩子
    if (cur->_kv.first > parent->_kv.first)
    {
        parent->_right = cur;
        cur->_parent = parent;

    }
    else
    {
        parent->_left = cur;
        cur->_parent = parent;
    }

    //...................................
    // 这些写更新平衡因子,和控制树的平衡的代码
    //...................................
    
    // 插入成功
    return true;
}

2.更新树的平衡因子

插入「新节点」,从该节点到根所经分支上的所有节点(即祖先节点)的平衡因子都有可能会受到影响,根据不同情况,更新它们的平衡因子:

  1. 如果插入在「新节点父亲」的右边,父亲的平衡因子++( _bf++
  2. 如果插入在「新节点父亲」的左边,父亲的平衡因子–( _bf--

「新节点父亲」的平衡因子更新以后,又会分为 3 种情况:

请添加图片描述

while (parent) // 最坏情况:更新到根节点
{
    // 更新新节点父亲的平衡因子

    if (cur == parent->_left) // 新节点插入在父亲的左边
    {
        parent->_bf--;
    }
    else // 新节点插入在父亲的右边
    {
        parent->_bf++;
    }

    // 检查新节点父亲的平衡因子

    // 1、父亲所在子树高度变了,需要继续往上更新
    if (parent->_bf == 1 || parent->_bf == -1)
    {
        cur = parent;
        parent = cur->_parent;
    }
    // 2、父亲所在子树高度没变,不用继续往上更新
    else if (parent->_bf == 0)
    {
        break;
    }
    // 3、父亲所在子树出现了不平衡,需要旋转处理
    else if (parent->_bf == 2 || parent->_bf == -2)
    {
        // 这里写对树进行平衡化操作,旋转处理的代码,分为4种情况:
        
        /*................................................*/
        // 3.1、父节点的左边高,右边低,需要往右旋
        if (parent->_bf == -2 && cur->_bf == -1)
        {
            // 右单旋
            treeRotateRight(parent); 
        }
        
        // 3.2、父节点的右边高,左边低,需要往左旋
        else if (parent->_bf == 2 && cur->_bf == 1)
        {
            // 左单旋
            treeRotateLeft(parent); 
        }
        
        // 3.3、父节点的左边高,且父节点左孩子的右边高
        else if(parent->_bf == -2 && cur->_bf == 1)
        {
            // 左右双旋
            treeRotateLR(parent);
        }
        
        // 3.4、父节点的右边高,且父节点右孩子的左边高
        else if(parent->_bf == 2 && cur->_bf == -1)
        {
            // 右左双旋
            treeRotateRL(parent);
        }
        
        else // 只有上述4种情况,没有其它情况,所以这里直接报错处理
        {
            assert(false);
        }
        
        break; // 旋转完成,树已平衡,退出循环
        
        /*................................................*/
    }
    // 4、除了上述3种情况,平衡因子不可能有其它的值,报错处理
    else
    {
        assert(false);
    }
}

3.根据更新后BF的情况,进行平衡化操作

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为4种:

补充:

  1. 旋转的本质:在遵循二叉搜索树的规则下,让左右均衡,降低整棵树的高度
  2. 该进行哪种旋转操作?- - - 引发旋转的路径是直线就是单旋,如果是折线就是双旋
  3. 我们看到的树,可能是一颗完整的树,也可能是一颗子树

第一种旋转右单旋(新节点插入较高左子树的最左侧)

请添加图片描述

上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左子树高度增加了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子树增加一层,即将左子树往上提,这样60转下来,因为60比30大,只能让其成为30的右子树,而如果30有右子树,右子树根的值一定大于30,小于60,只能让其成为60的左子树,旋转完成后,更新节点的平衡因子即可

引发右单旋的条件:

  1. 父亲左边高,右边低,所以要让父亲往右旋
  2. parent 的平衡因子为 -2,parent 左孩子平衡因子为 -1,观察发现,平衡因子都是负数,说明是左边高,也说明了==【引发旋转的路径是一条直线】==,所以我们要右旋操作

右单旋操作步骤:

  1. 让 subL 的右子树 subLR 成为 parent 的左子树(因为 subLR 的右子树根节点值大于30,小于60)
  2. 让 parent 成为 subL 的右子树(因为60大于30)
  3. 让 subL 变成这个子树的根
  4. 这一步操作前需要先判断下:parent 是根节点,还是一个普通子树
  5. 如果是根节点,则更新 subL 为新的根
  6. 如果是普通子树(可能是某个节点的左子树,也可能是右子树,这里需要判断下),然后更新 subL 为这个子树的根节点
  7. 根据树的结构,更新 parent 和 subL 的平衡因子为0

在旋转过程中,更新双亲指针的指向,有以下几种情况需要考虑:

  1. subL 的右子树 subLR 可能存在,也可能为空。(当不为空时才更新 subL 右子树 subLR 的双亲指针指向)
  2. 旋转完成后,subL 的双亲节点,可能是空,也可能是 parent 原先的父节点。(所以更新 subL 的双亲指针前需要判断下)

右单旋实现代码:

// 右单旋
void treeRotateRight(Node* parent)
{
    // subL:parent的左孩子
    // subLR:parent左孩子的右孩子
    Node* subL = parent->_left;
    Node* subLR = parent->_left->_right;

    // 1、让subL的右子树subLR成为parent的左子树
    parent->_left = subLR;
    // 1.1、如果subLR不为空
    if (subLR)
    {
        subLR->_parent = parent; // 更新subLR的双亲指针,指向parent
    }

    // 2、让parent成为subL的右子树
    subL->_right = parent;

    // 2.1、记录下parent的父节点
    Node* ppNode = parent->_parent;

    // 2.2、更新parent的双亲指针,指向subL
    parent->_parent = subL;

    // 2.3、判断parent是不是根节点
    // 是根节点
    if (parent == _root)
    {
        _root = subL;            // 更新subL为新的根
        subL->_parent = nullptr; // 更新subL的双亲指针,指向空
    }
    // 不是根节点,就是一个普通子树
    else
    {
        // 判断parent原先是左孩子还是右孩子
        if (ppNode->_left == parent)
        {
            ppNode->_left = subL; // parent原先的双亲节点接管subL,subL为这个子树的根
        }
        else
        {
            ppNode->_right = subL;
        }

        subL->_parent = ppNode; // 更新subL的双亲指针
    }

    // 根据调整后的结构更新parent和subL的平衡因子
    parent->_bf = subL->_bf = 0;
}

第二种旋转左单旋(新结点插入较高右子树的最右侧)

请添加图片描述

引发左单旋的条件:

  1. 父亲右边高,左边低,所以要让父亲往左旋
  2. parent 的平衡因子为 2,parent 右孩子平衡因子为 1,观察发现,平衡因子都是正数,说明是右边高,也说明了==【引发旋转的路径是一条直线】==,所以我们要左旋操作

左单旋操作步骤:

  1. 让 subR 的左子树 subRL 成为 parent 的右子树(因为 subRL 的左子树根节点值大于30,小于60)
  2. 让 parent 成为 subR 的左子树(因为30小于60)
  3. 让 subR 变成这个子树的根
    • 这一步操作前需要先判断下:parent 是根节点,还是一个普通子树
    • 如果是根节点,则更新 subR 为新的根
    • 如果是普通子树(可能是某个节点的左子树,也可能是右子树,这里需要判断下),然后更新 subR 为这个子树的根节点
  4. 根据树的结构,更新 parent 和 subR 的平衡因子为0

在旋转过程中,更新双亲指针的指向,有以下几种情况需要考虑:

  1. subR 的左子树 subRL 可能存在,也可能为空。(当不为空时才更新 subR 左子树 subRL 的双亲指针指向)
  2. 旋转完成后,subR 的双亲节点,可能是空,也可能是 parent 原先的父节点。(所以更新 subR 的双亲指针前需要判断下)

左单旋实现代码:

// 左单旋
void treeRotateLeft(Node* parent)
{
    // subR:父亲的右孩子
    // subRL:父亲的右孩子的左孩子(大于父亲,小于subR)
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    // 1、让subRL成为父亲的右子树
    parent->_right = subRL;
    // 如果subRL不为空
    if (subRL)
    {
        subRL->_parent = parent; // 更新subRL双亲指针,指向parent
    }

    // 2、让parent成为subR的左子树
    subR->_left = parent;

    // 2.1、先记录下parent的双亲节点
    Node* ppNode = parent->_parent;

    // 2.2、更新parent双亲指针的指向
    parent->_parent = subR;

    // 2.3、判断parent是不是根节点
    // 是根节点
    if (parent == _root)
    {
        _root = subR;            // subR为新的根
        subR->_parent = nullptr; // subR双亲指针指向空
    }
    // 不是根节点,就是一个普通子树
    else
    {
        // 判断parent原先是左孩子还是右孩子
        if (ppNode->_left == parent)
        {
            ppNode->_left = subR; // parent原先的双亲节点接管subR,subR为这个子树的根
        }
        else
        {
            ppNode->_right = subR;
        }

        subR->_parent = ppNode; // 更新subR的双亲指针
    }

    // 根据树的结构,更新parent和subR的平衡因子
    parent->_bf = subR->_bf = 0;
}

第三种旋转:左右双旋(新结点插入较高左子树的右侧)

请添加图片描述

下图是 h = 0 的情况:

请添加图片描述

引发双旋的条件:

  1. 引发旋转的路径是直线就是单旋,如果是折线就是双旋
  2. parent 的平衡因子为 -2,parent 左孩子平衡因子为 1,观察发现,平衡因子是一负一正,说明「左孩子右边高」,「父亲左边高」,也说明了==【引发旋转的路径是一条折线】==,所以我们要先「对左孩子进行左旋操作」,再「对父亲进行右旋操作」

左右双旋操作后,根据树的结构,更新平衡因子时,需要注意:

插入新节点的位置不同,经过左右双旋后,得到树的结构也会有所不同,平衡因子也会有所不同,有以下三种情况:

  1. 新节点插入到了「parent 左孩子的右子树」的
  2. 新节点插入到了「parent 左孩子的右子树」的
  3. 新节点就是「parent 左孩子的右孩子」

请添加图片描述

左右双旋实现代码:

// 左右双旋
void treeRotateLR(Node* parent)
{
    Node* subL = parent->_left; // 记录parent的左孩子
    Node* subLR = subL->_right; // 记录parent的左孩子的右孩子

    // 旋转之前,因为插入新节点的位置不同,subLR的平衡因子可能是-1/0/1

    int bf = subLR->_bf; // 记录subLR的平衡因子

    treeRotateLeft(parent->_left); // 先对parent的左孩子进行左单旋
    treeRotateRight(parent);       // 再对parent进行右单旋

    // 旋转完成之后,根据情况对其他节点的平衡因子进行调整
    if (bf == -1)
    {
        parent->_bf = 1;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else if (bf == 1)
    {
        parent->_bf = 0;
        subL->_bf = -1;
        subLR->_bf = 0;
    }
    else if (bf == 0)
    {
        parent->_bf = 0;
        subL->_bf = 0;
        subLR->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

第四种旋转:右左双旋(新节点插入较高右子树的左侧)

请添加图片描述

这是 h = 1 的情况:

请添加图片描述

引发双旋的条件:

  1. 引发旋转的路径是直线就是单旋,如果是折线就是双旋
  2. parent 的平衡因子为 2, parent 右孩子平衡因子为 -1,观察发现,平衡因子是一正一负,说明「右孩子左边高」,「父亲右边高」,也说明了==【引发旋转的路径是一条折线】==,所以我们要先「对右孩子进行右旋操作」,再「对父亲进行左旋操作」

左右双旋操作后,根据树的结构,更新平衡因子时,需要注意:

插入新节点的位置不同,经过右左双旋后,得到树的结构也会有所不同,平衡因子也会有所不同,有以下三种情况:

  1. 新节点插入到了「parent 右孩子的左子树」的左边
  2. 新节点插入到了「parent 右孩子的左子树」的右边
  3. 新节点就是「parent 右孩子的左孩子」

请添加图片描述

右左双旋实现代码:

// 右左双旋
void treeRotateRL(Node* parent)
{
    Node* subR = parent->_right; // 记录parent的右孩子
    Node* subRL = subR->_left;   // 记录parent的右孩子的左孩子

    // 旋转之前,因为插入新节点的位置不同,subRL的平衡因子可能为-1/0/1

    int bf = subRL->_bf; // 记录subRL的平衡因子

    treeRotateRight(parent->_right); // 先对parent的右孩子进行右单旋
    treeRotateLeft(parent);          // 再对parent进行左单选

    // 旋转完成之后,根据树的结构对其他节点的平衡因子进行调整
    if (bf == -1)
    {
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    }
    else if (bf == 1)
    {
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else if(bf == 0)
    {
        parent->_bf = 0;
        subR->_bf = 0;
        subRL->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

5.AVL树的验证操作

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

  1. 验证其是否为二叉搜索树
    • 如果中序遍历可以得到一个有序的序列,就说明为二叉搜索树
  2. 验证其是否为平衡树
    • 节点的平衡因子是否计算正确,每个节点子树高度差的绝对值不超过1

验证步骤:

(1)首先写一个计算当前树高度的函数

// 计算当前树的高度
int Height(Node* root)
{
    // 当前树为空,则高度为0
    if (root == nullptr)
        return 0;

    // 当前树不为空,计算左右子树的高度
    int leftHeight = Height(root->_left);
    int rightHeight = Height(root->_right);

    // 当前树的高度 = 左右子树中高度最大的那个加1
    return max(leftHeight, rightHeight) + 1;
}

(2)检查AVL树是否平衡,思路一:自顶向下的暴力解法

// 检查AVL树是否平衡,思路一
bool IsBalance1()
{
    return _IsBalance1(_root);
}
bool _IsBalance1(Node* root)
{
    // 当前树为空,说明是平衡的
    if (root == nullptr)
        return true;

    // 当前树不为空,计算左右子树的高度
    int leftHeight = Height(root->_left);
    int rightHeight = Height(root->_right);

    if (rightHeight - leftHeight != root->_bf) // 检查当前树的平衡因子是否计算正确
    {
        cout << "平衡因子异常:" << root->_kv.first << endl;
    }
    
    // 左右子树高度相减的绝对值小于2,说明当前树是平衡的,则继续往下判断其它子树
    return abs(leftHeight - rightHeight) < 2
        && _IsBalance1(root->_left)
        && _IsBalance1(root->_right);
}

(3)检查AVL树是否平衡,思路二:自底向上的高效解法(动态规划,前一个子问题的解,能够用于后一个问题求解)

// 检查AVL树是否平衡,思路二
bool IsBalance2()
{
    return _IsBalance2(_root) != -1;
}

int _IsBalance2(Node* root)
{
    // 先判断当前树的左、右子树是否平衡,再判断当前树是否平衡
    // 不平衡返回-1,平衡则返回当前树的高度

    // 当前树为空,返回高度0
    if (root == nullptr)
        return 0;

    // 当前树不为空,分别计算左右子树的高度
    int leftHeight = _IsBalance2(root->_left);
    int rightHeight = _IsBalance2(root->_right);
    
    if (rightHeight - leftHeight != root->_bf) // 检查当前树的平衡因子是否计算正确
    {
        cout << "平衡因子异常:" << root->_kv.first << endl;
    }
    
    // 左子树高度等于-1、右子树高度等于-1、左右子树高度差的绝对值大于1,说明当前树不平衡
    if (leftHeight == -1 || rightHeight == -1 || abs(leftHeight - rightHeight) > 1)
        return -1;

    // 运行到这里来了,说明当前树是平衡的,返回当前树的高度
    return max(leftHeight, rightHeight) + 1;
}

6.AVL树的删除结点操作(了解)

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,如果出现不平衡树,进行旋转。只不过与二叉搜索树不同的是,AVL树删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置


7.AVL树的性能

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

效率:O(log2N)

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

「已注销」

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值