【C++】AVL树

简介: 【C++】AVL树

前言

本文章将会模拟实现一棵AVL树。


以下是本篇文章正文内容

一、什么是AVL树?

AVL树也是一个二叉搜索树,只不过是在二叉搜索树的基础上,增加了一个条件:

任意一棵子树的左右高度差的绝对值不大于1。


设计AVL树的原因

在二叉平衡搜索树中,在正常情况下,对该树的任意节点进行查找,最多查找OlogN次,但如果在极端情况下,该树退化成了单只树,则会极大降低搜索效率,变成O(n),所以为了避免让二叉搜索树出现极端情况,设计出一棵具有平衡性质的二叉搜索树:AVL树


二、AVL树的性质

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

平衡因子:右子树高度 - 左子树的高度(注意是右 - 左)

由此可知,一棵AVL树是高度平衡的,它的高度可保持在logN,所以搜索效率可以保持在logN


三、二叉树节点的定义

template<class K, class V>
class AVLTreeNode
{
public:
  AVLTreeNode(const pair<K, V> kv)
    :_kv(kv)
    ,_left(nullptr)
    ,_right(nullptr)
    ,_parent(nullptr)
    ,_bf(0)
  {}
  pair<K, V> _kv;
  AVLTreeNode<K, V>* _left;
  AVLTreeNode<K, V>* _right;
  AVLTreeNode<K, V>* _parent;
  int _bf;
};
  • 1)需要有一个平衡因子存在,即_bf
  • 2)_data值可以是一个键值对,也可以是其他类型,这里我选择了pair
  • 3)在后面的其他操作中,会频繁用到一个节点的父亲,所以直接在节点中添加一个_parent成员。

四、AVL树的插入

插入大致分为几个步骤:

如果根节点为空,则直接插入到根节点的位置

1)先找到插入位置,因为这是一棵搜索树,如果该节点的值比根小,则往左走,如果比根大,往右边走。

2)找到待插入位置后,插入该节点,然后调整平衡因子。

如果插入的是左边,则parent的平衡因子–,如果插入的是右边,则parent的平衡因子++。

如果parent的平衡因子是1或-1,说明父亲的子树有一边高了,则需要继续向上调整。(最坏情况就是调整平衡因子到根的位置)

如果parent的parent的平衡因子大于1或者小于-1了,则不满足AVL树的特性,需要进行旋转。

旋转

1)右单旋

新插入的节点在根节点的左侧,导致根节点的平衡因子变成-2。

则需要将根节点进行右旋。

规则如下:

  • 1)cur的right给parent的left
  • 2)parent变成了cur的right
    调整后,cur变成了新的根,parent变成cur的right
    此时需要注意一些细节:

如果在旋转前,parent不是根,也就是如果parent还是某一个节点的孩子,则在旋转后cur变成新的根,需要替代parent的位置,变成那个节点的孩子。

如果旋转前parent就是根,则直接让旋转后的cur的parent为空即可。

调整后cur和parent的平衡因子变成0。

2)左单旋

新插入的节点在根节点的右侧,导致根节点的平衡因子变成2。

则需要将根节点进行左旋。

规则如下:

  • 1)cur的left给parent的right
  • 2)parent变成了cur的left
    调整后,cur变成了新的根,parent变成curr的left
    此时需要注意一些细节:

如果在旋转前,parent不是根,也就是如果parent还是某一个节点的孩子,则在旋转后cur变成新的根,需要替代parent的位置,变成那个节点的孩子。

如果旋转前parent就是根,则直接让旋转后的cur的parent为空即可。

调整后cur和parent的平衡因子变成0。

3)左右双旋

插入新节点后,如果cur的平衡因子为1,parent的平衡因子为-2

说明整颗树的结构大概是这样的:

这就说明,此时这棵树不再是AVL树,所以我们需要对其进行旋转。

旋转操作如下:

  • 1)让cur的右指向curright的左,然后cur成为curright的左。此时curright的父亲就变成了原来的cur的父亲,cur的父亲变成了curright

以上操作是左旋的操作过程

  • 2)让parent的左指向curright的右,然后parent变成了curright的右,此时parent的父亲变成了curright。

以上操作是右旋的操作过程

这里还需要注意一些细节:

如果在旋转前,parent不是根,也就是如果parent还是某一个节点的孩子,则在旋转后curright变成新的根,需要替代parent的位置,变成那个节点的孩子。

平衡因子的处理:

  • 1)如果旋转之前,curright的平衡因子是0,则说明,curright这个节点,一定是新插入的节点。

(因为如果不是新插入的节点,在插入该节点前,这棵树就不是AVL树了)。

在进行左右双旋后,cur,curright,parent三个节点的平衡因子都是0。

  • 2)如果旋转之前,curright这个节点的平衡因子是-1,该情况就是上图画出的情况,说明新插入节点在curright的左子树,在左右双旋后,cur和curright的平衡因子变成0,parent的平衡因子是1
  • 3)如果旋转之前,curright这个节点的平衡因子是1,说明新插入节点在curright的右子树,在左右双旋后,parent和curright的平衡因子变成0,cur的平衡因子是-1

4)右左双旋

插入新节点后,如果cur的平衡因子为-1,parent的平衡因子为2

说明整颗树的结构大概是这样的:

这就说明,此时这棵树不再是AVL树,所以我们需要对其进行右左旋转。

旋转操作如下:

  • 1)让cur的左指向curleft的右,然后cur成为curleft的右。此时culeft的父亲就变成了原来的cur的父亲,cur的父亲变成了curleft

以上操作是右旋的操作过程

  • 2)让parent的右指向curleft的左,然后parent变成了curleft的左,此时parent的父亲变成了curleft。

以上操作是左旋的操作过程

这里还需要注意一些细节:

如果在旋转前,parent不是根,也就是如果parent还是某一个节点的孩子,则在旋转后currleft变成新的根,需要替代parent的位置,变成那个节点的孩子。

平衡因子的处理:

  • 1)如果旋转之前,curleft的平衡因子是0,则说明,curleft这个节点,一定是新插入的节点。

(因为如果不是新插入的节点,在插入该节点前,这棵树就不是AVL树了)。

在进行左右双旋后,cur,curleft,parent三个节点的平衡因子都是0。

  • 2)如果旋转之前,curleft这个节点的平衡因子是1,该情况就是上图画出的情况,说明新插入节点在curleft的右子树,在右左双旋后,cur和curleft的平衡因子变成0,parent的平衡因子是-1
  • 3)如果旋转之前,curleft这个节点的平衡因子是-1,说明新插入节点在curleft的左子树,在右左双旋后,parent和curleft的平衡因子变成0,cur的平衡因子是1

以上就是旋转的四种情况。

AVL树插入完整代码

template<class K,class V>
class AVLTree
{
  typedef AVLTreeNode<K, V> Node;
public:
  AVLTree()
    :_root(nullptr)
  {}
  bool Insert(const pair<K,V> kv)
  {
    if(_root == nullptr)
    {
      _root = new Node(kv);
      return true;
    }
    Node* cur = _root;
    Node* cur_parent = _root;
    //1.找到待插入位置
    while (cur)
    {
      if (cur->_kv.first < kv.first)
      {
        cur_parent = cur;
        cur = cur->_right;
      }
      else if (cur->_kv.first > kv.first)
      {
        cur_parent = cur;
        cur = cur->_left;
      }
      else
        return false;
    }
    //2.先判断待插入节点是在parent的左边还是右边
    cur = new Node(kv);
    if (cur_parent->_kv.first > kv.first)
    {
      cur_parent->_left = cur;
    }
    else
    {
      cur_parent->_right = cur;
    }
    cur->_parent = cur_parent;
    //下面为调整二叉树的平衡
    //1.更新平衡因子
    //
    while (cur_parent)
    {
      //左子树--
      if (cur_parent->_left == cur)
      {
        cur_parent->_bf--;
      }
      //右子树++
      else
      {
        cur_parent->_bf++;
      }
      //平衡因子=0,不再影响祖先
      if (cur_parent->_bf == 0)
      {
        break;
      }
      //不平衡了,影响祖先,要向上也调整
      else if (cur_parent->_bf == 1 || cur_parent->_bf == -1)
      {
        cur = cur_parent;
        cur_parent = cur_parent->_parent;
      }
      //此时该树出问题了,需要旋转进行平衡
      else if (cur_parent->_bf == 2 || cur_parent->_bf == -2)
      {
          //右边高,向左旋转
        if (cur_parent->_bf == 2 && cur->_bf == 1)
        {
          RotateL(cur_parent);
        }
        //左边高,向右旋转
        else if (cur_parent->_bf == -2 && cur->_bf == -1)
        {
          RotateR(cur_parent);
        }
        //折线型,右边高,右左双旋
        else if (cur_parent->_bf == 2 && cur->_bf == -1)
        {
          RotateRL(cur_parent);
        }
        //折线形,左边高,左右双旋
        else if (cur_parent->_bf == -2 && cur->_bf == 1)
        {
          RotateLR(cur_parent);
        }
        //旋转完成后一定平衡了,则break
        break;
      }
      else
      {
        assert(false);
      }
    }
    cout << kv.first << endl;
    return true;
  }
  //左单旋
  void RotateL(Node* parent)
  {
    Node* cur = parent->_right;
    Node* curleft = cur->_left;
    Node* ppNode = parent->_parent;
    parent->_right = curleft;
    if (curleft)
      curleft->_parent = parent;
    cur->_left = parent;
    parent->_parent = cur;
    if (!ppNode)
    {
      _root = cur;
      cur->_parent = nullptr;
    }
    else
    {
      if (ppNode->_right == parent)
      {
        ppNode->_right = cur;
      }
      else
      {
        ppNode->_left = cur;
      }
      cur->_parent = ppNode;
    }
    cur->_bf = parent->_bf = 0;
  }
  //右单旋
  void RotateR(Node* parent)
  {
    Node* cur = parent->_left;
    Node* curright = cur->_right;
    Node* ppNode = parent->_parent;
    parent->_left = curright;
    //如果cur的右子树是空
    if(curright)
      curright->_parent = parent;
    cur->_right = parent;
    parent->_parent = cur;
    if (!ppNode)
    {
      _root = cur;
      cur->_parent = nullptr;
    }
    else
    {
      cur->_parent = ppNode;
      //要知道根的左边是cur还是右边是cur
      if (ppNode->_left == parent)
      {
        ppNode->_left = cur;
      }
      else
      {
        ppNode->_right = cur;
      }
    }
    cur->_bf = parent->_bf = 0;
  }
  void RotateLR(Node* parent)
  {
    Node* cur = parent->_left;
    Node* curright = cur->_right;
    int bf = curright->_bf;
    RotateL(parent->_left);
    RotateR(parent);
    //该节点为新插入的节点
    if (bf == 0)
    {
      curright->_bf = 0;
      cur->_bf = 0;
      parent->_bf = 0;
    }
    //新插入节点在curright的右边,右边高了
    //     p
    //   c
    //     cr
    else if (bf == 1)
    {
      curright->_bf = 0;
      cur->_bf = -1;
      parent->_bf = 0;
    }
    //新插入节点在curright的左边,左边高了
    else if (bf == -1)
    {
      curright->_bf = 0;
      cur->_bf = 0;
      parent->_bf = 1;
    }
  }
  void RotateRL(Node* parent)
  {
    Node* cur = parent->_right;
    Node* curleft = cur->_left;
    int bf = curleft->_bf;
    RotateR(parent->_right);
    RotateL(parent);
    //该节点为新插入的节点
    if (bf == 0)
    {
      curleft->_bf = 0;
      cur->_bf = 0;
      parent->_bf = 0;
    }
    //新插入节点在curleft的右边,右边高了
    //     p
    //      c
    //     cl
    else if (bf == 1)
    {
      curleft->_bf = 0;
      cur->_bf = 0;
      parent->_bf = -1;
    }
    //新插入节点在curleft的左边,左边高了
    else if (bf == -1)
    {
      curleft->_bf = 0;
      cur->_bf = 1;
      parent->_bf = 0;
    }
  }
private:
  Node* _root = nullptr;
};

验证一棵树为AVL树

  • 1)验证这棵树的中序遍历是有序的。
  • 2)验证每一棵树的左右子树高度差不大于1
int Height(Node* root)
{
  if (root == nullptr)
    return 0;
  int left = Height(root->_left);
  int right = Height(root->_right);
  return left > right ? left + 1 : right + 1;
}
bool IsBalanceTree()
{
  cout << "IsBalanceTree():";
  return _IsBalanceTree(_root);
}
//通过高度判断是否为AVL树
//1.通过高度计算出真实的平衡因子,再与AVL树本身的平衡因子进行比较
bool _IsBalanceTree(Node* root)
{
  if (root == nullptr)
    return true;
  int lefth = Height(root->_left);
  int righth = Height(root->_right);
  int bf = righth - lefth;
  if (bf != root->_bf || bf > 1 || bf < -1)
  {
    return false;
  }
  return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

AVL树的性能分析

由于AVL树的绝对平衡(每棵树高度差不大于1),每次在插入数据时,难免会遇到多次旋转,最坏情况需要旋转到根部。虽然旋转时间复杂度O(1),但如果旋转次数过多,也会造成效率下降。在本文中没有提到AVL树的删除,删除操作更加复杂,我没有研究过hh,不过同样每删除一个数据,都必须保证整棵树是AVL树,这又需要大量旋转来维持它的平衡。

所以在面对大量数据,并且不再有新数据的插入时,可以使用AVL树进行查找,效率为O(logN).

总结

本文主要讲述了AVL树的插入过程及其效率分析。

相关文章
|
16天前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树,由Georgy Adelson-Velsky和Evgenii Landis提出。它通过确保任意节点的两子树高度差不超过1来维持平衡,支持高效插入、删除和查找操作,时间复杂度为O(log n)。AVL树通过四种旋转操作(左旋、右旋、左-右旋、右-左旋)来恢复树的平衡状态,适用于需要频繁进行数据操作的场景。
29 2
|
3月前
|
存储 C++
【C++】AVL树
AVL树是一种自平衡二叉搜索树:它以苏联科学家Georgy Adelson-Velsky和Evgenii Landis的名字命名。
28 2
|
4月前
|
C++ 容器
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
36 5
|
5月前
|
C++
【C++】手撕AVL树(下)
【C++】手撕AVL树(下)
55 1
|
5月前
|
算法 测试技术 C++
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
39 2
|
5月前
|
Java C++ Python
【C++】手撕AVL树(上)
【C++】手撕AVL树(上)
57 0
|
6月前
|
C++
【c++】avl树
【c++】avl树
39 0
|
14天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
25 2
|
20天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
54 5
|
26天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
56 4