AVL 树详解:从概念到 C++ 实现

文章目录

    • [1. 引言](#1. 引言)
    • [2. AVL 树的概念](#2. AVL 树的概念)
      • [2.1 平衡因子](#2.1 平衡因子)
      • [2.2 为什么高度差不超过 1 而不是 0?](#2.2 为什么高度差不超过 1 而不是 0?)
    • [3. AVL 树的实现](#3. AVL 树的实现)
      • [3.1 AVL 树的结构](#3.1 AVL 树的结构)
      • [3.2 AVL 树的插入](#3.2 AVL 树的插入)
        • [3.2.1 插入一个值的大概过程](#3.2.1 插入一个值的大概过程)
        • [3.2.2 平衡因子更新](#3.2.2 平衡因子更新)
        • [3.2.3 插入结点及更新平衡因子的代码实现](#3.2.3 插入结点及更新平衡因子的代码实现)
    • [4. 旋转](#4. 旋转)
    • [5. AVL 树的查找](#5. AVL 树的查找)
    • [6. AVL 树平衡检测](#6. AVL 树平衡检测)

1. 引言

AVL 树是最先发明的自平衡二叉查找树,由前苏联科学家 G. M. Adelson-Velsky 和 E. M. Landis 在 1962 年的论文《An algorithm for the organization of information》中提出。它通过控制左右子树的高度差来维持平衡,从而保证增删查改的效率稳定在 O(log N),相比普通二叉搜索树有了本质的提升。

今天我将从 AVL 树的核心概念出发,带你逐步理解平衡因子的更新、四种旋转操作,并给出完整的 C++ 实现代码。

2. AVL 树的概念

AVL 树是一颗空树,或者具备下列性质的二叉搜索树:

  • 它的左右子树都是 AVL 树;
  • 左右子树的高度差的绝对值不超过 1。

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

2.1 平衡因子

AVL 树实现中我们引入一个平衡因子 的概念。每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度 ,也就是说任何结点的平衡因子等于 0/1/-1。

AVL 树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。

2.2 为什么高度差不超过 1 而不是 0?

思考一下为什么 AVL 树是高度平衡搜索二叉树,要求高度差不超过 1,而不是高度差是 0 呢?0 不是更好的平衡吗?

画画图分析我们发现,不是不想这样设计,而是有些情况是做不到高度差是 0 的。比如一棵树是 2 个结点、4 个结点等情况下,高度差最好就是 1,无法做到高度差是 0。

AVL 树整体结点数量和分布和完全二叉树类似,高度可以控制在 log N,那么增删查改的效率也可以控制在 O(log N),相比二叉搜索树有了本质的提升。

3. AVL 树的实现

3.1 AVL 树的结构

cpp 复制代码
template<class K, class V>
struct AVLTreeNode
{
    // 需要 parent 指针,后续更新平衡因子可以看到
    pair<K, V> _kv;
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    AVLTreeNode<K, V>* _parent;
    int _bf; // balance factor平衡因子

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

template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    //...
private:
    Node* _root = nullptr;
};

3.2 AVL 树的插入

3.2.1 插入一个值的大概过程
  1. 插入一个值按二叉搜索树规则进行插入。

  2. 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点到根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了。

  3. 更新平衡因子过程中没有出现问题,则插入结束。

  4. 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。

3.2.2 平衡因子更新

更新原则:

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

  • 只有子树高度变化才会影响当前结点平衡因子;

  • 插入结点会增加高度,所以新增结点在 parent 的右子树,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 也停止了。

更新到10结点,平衡因⼦为2,10所在的⼦树已经不平衡,需要旋转处理

更新到中间结点,3为根的⼦树⾼度不变,不会影响上⼀层,更新结束

最坏更新到根停⽌

3.2.3 插入结点及更新平衡因子的代码实现
cpp 复制代码
bool Insert(const 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)
        {
            // 不平衡了,旋转处理
            break;
        }
        else
        {
            assert(false);
        }
    }
    return true;
}

4. 旋转

4.1 旋转的原则

  1. 保持搜索树的规则;
  2. 让旋转的树从不满足变平衡 ,其次降低旋转树的高度。

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

说明:下面的图中,有些结点我们给的是具体值,如 10 和 5 等结点,这里是为了方便讲解,实际中是什么值都可以,只要大小关系符合搜索树的性质即可。

4.2 右单旋

本图1展示的是 10 为根的树,有 a/b/c 抽象为三棵高度为 h 的子树(h>=0),a/b/c 均符合 AVL 树的要求。10 可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里 a/b/c 是高度为 h 的子树,是一种概括抽象表示,他代表了所有右单旋的场景。

在 a 子树中插入一个新结点,导致 a 子树的高度从 h 变成 h+1,不断向上更新平衡因子,导致 10 的平衡因子从 -1 变成 -2,10 为根的树左右高度差超过 1,违反平衡规则。10 为根的树左边太高了,需要往右边旋转,控制两棵树的平衡。

旋转核心步骤:因为 5 < b 子树的值 < 10,将 b 变成 10 的左子树,10 变成 5 的右子树,5

变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的 h+2,符合旋转原则。如果插入之前 10

是整棵树的一个局部子树,旋转后不会再影响上一层,插入结束了。

a/b/c三棵树就提可以有下面几种情况:

右单旋代码实现
cpp 复制代码
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;
}

4.3 左单旋

本图6展示的是 10 为根的树,有 a/b/c 抽象为三棵高度为 h 的子树(h>=0),a/b/c 均符合 AVL 树的要求。10 可能是整棵树的根,也可能是一个整棵树中局部的子树的根。

在 a 子树中插入一个新结点,导致 a 子树的高度从 h 变成 h+1,不断向上更新平衡因子,导致 10 的平衡因子从 1 变成 2,10 为根的树左右高度差超过 1,违反平衡规则。10 为根的树右边太高了,需要往左边旋转,控制两棵树的平衡。

旋转核心步骤:因为 10 < b 子树的值 < 15,将 b 变成 10 的右子树,10 变成 15 的左子树,15

变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的 h+2,符合旋转原则。

左单旋代码实现
cpp 复制代码
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;
}

4.4 左右双旋

通过图 7 和图 8 可以看到,左边高时,如果插入位置不是在 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 复制代码
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);
    }
}

4.5 右左双旋

跟左右双旋类似,下面我们将 a/b/c 子树抽象为高度 h 的 AVL 子树进行分析,另外我们需要把 b 子树的细节进一步展开为 12

和左子树高度为 h-1 的 e 和 f 子树,因为我们要对 b 的父亲 15 为旋转点进行右单旋,右单旋需要动 b 树中的右子树。b

子树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察 12 的平衡因子不同,这里我们要分三个场景讨论。

  • 场景 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 复制代码
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);
    }
}

5. AVL 树的查找

按二叉搜索树逻辑实现即可,搜索效率为 O(log N)。

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

6. AVL 树平衡检测

我们实现的 AVL 树是否合格,我们通过检查左右子树高度差的程序进行反向验证,同时检查一下结点的平衡因子更新是否出现了问题。

cpp 复制代码
1 int _Height(Node* root)
2 {
3 if (root == nullptr)
4 return 0;
5
6 int leftHeight = _Height(root->_left);
7 int rightHeight = _Height(root->_right);
8
9 return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
10 }
11
12 bool _IsBalanceTree(Node* root)
13 {
14 // 空树也是AVL树
15 if (nullptr == root)
16 return true;
17
18 // 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差
19 int leftHeight = _Height(root->_left);
20 int rightHeight = _Height(root->_right);
21 int diff = rightHeight - leftHeight;
22
23 // 如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者
24 // pRoot平衡因⼦的绝对值超过1,则⼀定不是AVL树
25 if (abs(diff) >= 2)
26 {
27 cout << root->_kv.first << "⾼度差异常" << endl;
28 return false;
29 }
30
31 if (root->_bf != diff)
32 {
33 cout << root->_kv.first << "平衡因⼦异常" << endl;
34 return false;
35 }
36
37 // pRoot的左和右如果都是AVL树,则该树⼀定是AVL树
38 return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
39 }
40
41 // 测试代码
42 void TestAVLTree1()
43 {
44 AVLTree<int, int> t;
45 // 常规的测试⽤例
46 //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
47 // 特殊的带有双旋场景的测试⽤例
48 int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
49 for (auto e : a)
50 {
51 t.Insert({ e, e });
52 }
53
54 t.InOrder();
55 cout << t.IsBalanceTree() << endl;
56 }
57
58 // 插⼊⼀堆随机值,测试平衡,顺便测试⼀下⾼度和性能等
59 void TestAVLTree2()
60 {
61 const int N = 100000;
62 vector<int> v;
63 v.reserve(N);
64 srand(time(0));
65
66 for (size_t i = 0; i < N; i++)
67 {
68 v.push_back(rand()+i);
69 }
70
71 size_t begin2 = clock();
72 AVLTree<int, int> t;
73 for (auto e : v)
74 {
75 t.Insert(make_pair(e, e));
76 }
77 size_t end2 = clock();
78
79 cout << "Insert:" << end2 - begin2 << endl;
80 cout << t.IsBalanceTree() << endl;
81
82 cout << "Height:" << t.Height() << endl;
83 cout << "Size:" << t.Size() << endl;
84
85 size_t begin1 = clock();
86 // 确定在的值
87 /*for (auto e : v)
88 {
89 t.Find(e);
90 }*/
91
92 // 随机值
93 for (size_t i = 0; i < N; i++)
94 {
95 t.Find((rand() + i));
96 }
97
98 size_t end1 = clock();
99
100 cout << "Find:" << end1 - begin1 << endl;
101 }
相关推荐
凉茶钱1 小时前
list:哨兵节点,const_iterator 和 const iterator的区别与迭代器失效
数据结构·c++·list
沫璃染墨1 小时前
从零入门计算机网络系列(六):TCP套接字编程实战——从listen监听到客户端与服务器通信
linux·服务器·c++·tcp/ip·计算机网络·tcp
努力实干2 小时前
C++ 面向对象与工程架构
开发语言·c++·架构
代钦塔拉2 小时前
【C++/Qt 进阶】设计模式系列(6):桥接模式——把“两个独立变化的维度“拆开
c++·qt·设计模式
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C/C++考级:任意进制互转与大数多进制加法解析
c++·python·算法·bfs·信息学竞赛
Shadow(⊙o⊙)2 小时前
仿muduo实现高并发服务器——EPOLL设置与移除Poller.hpp管理Channel
运维·服务器·c++
闻缺陷则喜何志丹2 小时前
【C++特性及类库】多线程
开发语言·c++·多线程·语言特性·系统库
多弗朗皮卡丘2 小时前
C++ 哈希(下)
c++·算法·哈希算法
dtq04243 小时前
C++3 - 分支语句if和string变量
开发语言·c++·学习