数据结构——AVL树

简介: 数据结构——AVL树

概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

因此,两位苏联的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

这里是在每个结点加入了一个平衡因子,这个平衡因子是通过右子树和左子树的高度差算出来的。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
它的左右子树都是AVL树左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

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

这棵树是平衡的,也就是说时间复杂度为logN,效率非常高。

节点定义

对于AVL树结点的定义,不仅仅多了一个平衡因子,还多了一个父节点的指针,是一个三叉链的结构。

template<class T,class V>
struct AVLTreeNode//AVL树结点
{
  AVLTreeNode(const pair<T, V>& data)
    :_left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _data(data)
    ,_factor(0)//刚创建的结点平衡因子是0,因为平衡因子只受子树影响
  { }
  AVLTreeNode* _left;//左子树结点
  AVLTreeNode* _right;//右子树结点
  AVLTreeNode* _parent;//父节结点
  pair<T, V> _data;//结点的值,KV模型
  int _factor;//平衡因子
};

插入

我们这里只研究插入即可。

插入首先要按照平衡二叉树那样找到对应的位置。

然后将他们链接起来。

连接起来之后还要更新平衡因子。

这里平衡因子已经是2或者-2了,没必要进行平衡因子的更新了。

这里就是最坏的情况了,可能需要更新到初始的根结点。

这里更改平衡因子其实就要用到parent结点了,这就是为什么需要一个_parent的指针了。

template<class T, class V>
class AVLTree//AVL树
{
  typedef AVLTreeNode<T, V> Node;
public:
  bool Insert(const pair<T, V>& x)
  {
    if (_root == nullptr)
    {
      _root = new Node(x);
      return true;
    }
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
      if (cur->_data.first > x.first)
      {
        parent = cur;
        cur = cur->_left;
      }
      else if(cur->_data.first < x.first)
      {
        parent = cur;
        cur = cur->_right;
      }
      else
      {
        return false;
      }
    }
    cur = new Node(x);
    if (parent->_data.first > cur->_data.first)
    {
      parent->_left = cur;
      cur->_parent = parent;
    }
    else
    {
      parent->_right = cur;
      cur->_parent = parent;
    }
    //这里调节平衡因子
    while (parent)//如果parent为空就说明到了初始根结点
    {
      if (parent->_left == cur)
      {
        parent->_factor--;//如果是父节点的左子树插入了新节点就--
      }
      else if (parent->_right == cur)
      {
        parent->_factor++; //如果是父节点的右子树插入了新节点就++
      }
      //旋转
      if (parent->_factor == 0)//这里要判断parent结点是不是0,是0就说明不需要向上调整
      {
        break;
      }
      else if (parent->_factor == 1 || parent->_factor == -1)//这里就需要向上调整平衡因子了
      {
        cur = parent;
        parent = parent->_parent;
      }
      else if (parent->_factor == 2 || parent->_factor == -2)//这里就需要去旋转了
      {
        if (parent->_factor == 2 && cur->_factor == 1)
        {
          RotateL(parent);
        }
        else if (parent->_factor == -2 && cur->_factor == -1)
        {
          RotateR(parent);
        }
        else if (parent->_factor == 2 && cur->_factor == -1)
        {
          RotateRL(parent);
        }
        else if (parent->_factor == -2 && cur->_factor == 1)
        {
          RotateLR(parent);
        }
        else//以防万一
        {
          assert(false);
        }
        break;
      }
      else//以防万一
      {
        assert(false);
      }
    }
    return true;
  }
private:
  void _Inorder(Node* _root)
  {
    if (_root == nullptr)
      return;
    _Inorder(_root->_left);
    cout << _root->_data.first << ":" << _root->_data.second << endl;
    _Inorder(_root->_right);
  }
  Node* _root = nullptr;//AVL树的根节点
};

旋转

旋转的目的;

1.让这棵树的左右树高度差不超过1

2.旋转之后也要保持这棵树是AVL树

3.更新调节平衡因子

4.旋转后的高度要和插入前相同

左单旋与右单旋

左单旋:

对于左单旋这张图针对的是很多种情况,下面我用三种情况举例子。

在60结点插入,无论是左边还是右边都会让30结点的平衡因子变成2,都会发生旋转。

无论在60结点的左子树的子树插入还是右子树的子树插入,都会影响30的平衡因子,都会引发旋转。

其实这三种情况我们能看出来,旋转的核心就是:

30结点变成60结点的左子树,原本60结点的左子树内容变成了30结点的右子树。

这里要注意,30不一定是根,有可能是局部的一个子树而已,所以需要储存30结点之前的结点,之后让30的父节点指向60,或者是根指向60。

那么如何实现代码呢?储存30的父节点,储存30结点的右子树和30结点的右子树的左子树。

void RotateL(Node* parent)//左单旋
{
  Node* pparent = parent->_parent;//30的父节点
  Node* subR = parent->_right;//60结点
  Node* subRL = subR->_left;//60结点的左子树
  parent->_right = subRL;//让30结点的右指针指向60结点的左子树
  if (subRL)//60的左子树如果为空就不能访问
    subRL->_parent = parent;
  subR->_left = parent;//让60结点的左指针指向30结点
  parent->_parent = subR;
  if (pparent)//让30的结点父节点指向60结点
  {
    if (pparent->_left == parent)
      pparent->_left = subR;
    else
      pparent->_right = subR;
  }
  else
  {
    _root = subR;
  }
  subR->_parent = pparent;//让60的结点与父节点链接
  parent->_factor = subR->_factor = 0;//调节平衡因子
}

右单旋:

void RotateR(Node* parent)//右单旋
{
  Node* pparent = parent->_parent;
  Node* subL = parent->_left;
  Node* subLR = subL->_right;
  parent->_left = subLR;
  if (subLR)
    subLR->_parent = parent;
  subL->_right = parent;
  parent->_parent = subL;
  if (pparent)
  {
    if (pparent->_left == parent)
      pparent->_left = subL;
    else
      pparent->_right = subL;
  }
  else
  {
    _root = subL;
  }
  subL->_parent = pparent;
  subL->_factor = parent->_factor = 0;
}

双旋转

右左

如果是这种情况怎么办呢?

上面的左单旋和右单旋已经行不通了。

其实只要先对3结点右单旋一次在对1结点左单旋一次就可以了。

这里难处理的不是过程,因为上面已经写过了,难处理的是平衡因子:

观察插入后的和最终结果的两个平衡因子,60结点的右子树给了90结点的左子树,60结点的左子树给了30结点的右子树。所以平衡因子也就能算出来了。

void RotateRL(Node* parent)//右左
{
  Node* subR = parent->_right;
  Node* subRL = subR->_left;
  int fb = subRL->_factor;//判断60结点哪边长
  RotateR(parent->_right);
  RotateL(parent);
  if (fb == 1)//新插入的地方是在60结点的右
  {
    subR->_factor = 0;
    subRL->_factor = 0;
    parent->_factor = -1;
  }
  else if (fb == -1)//新插入的地方是在60结点的左
  {
    subR->_factor = 1;
    subRL->_factor = 0;
    parent->_factor = 0;
  }
  else//60结点是新插入的结点
  {
    subR->_factor = 0;
    subRL->_factor = 0;
    parent->_factor = 0;
  }
}

左右

void RotateLR(Node* parent)//左右
{
  Node* subL = parent->_left;
  Node* subLR = subL->_right;
  int fb = subLR->_factor;
  RotateL(parent->_left);
  RotateR(parent);
  if (fb == 1)
  {
    subL->_factor = -1;
    subLR->_factor = 0;
    parent->_factor = 0;
  }
  else if (fb == -1)
  {
    subL->_factor = 0;
    subLR->_factor = 0;
    parent->_factor = 1;
  }
  else
  {
    subL->_factor = 0;
    subLR->_factor = 0;
    parent->_factor = 0;
  }
}

总结:

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

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

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

验证AVL树

这里还需要加一个平衡因子的判断;

int _Height(Node* root)//计算树的高度
  {
    if (root == nullptr)
      return 0;
    int l = _Height(root->_left);
    int r = _Height(root->_right);
    return l > r ? l + 1 : r + 1;//返回左子树和右子树最高高度
  }
  bool _IsBalanceTree(Node* root)
  {
    if (root == nullptr)//空树也是AVL树
      return true;
    int L = _Height(root->_left);
    int R = _Height(root->_right);
    int diff = R - L;//右减去左的高度
    if (diff != root->_factor || (diff > 1 || diff < -1))//检查这个结点的左右子树差是否合法
    {
      cout << root->_data.first << ":" << "平衡因子异常" << ":" << root->_factor << endl;
      return false;
    }
    return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);//整棵树的每个节点都要检查
  }

然后用两个测试用例试一下:

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

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

删除(了解)

其实删除就是类似于插入的流程,只不过更复杂了一些,需要逆向思维去推理。

假如删除9结点,对于8结点来说就要减减,删除左边就是加加。

这里8的结点平衡因子就是0了,这说明高度变了,所以需要继续往上调整平衡因子。

如果是删除6结点,那么也是四种旋转的方式。

如果是删除7结点,那就是替换法。

相关文章
|
1月前
|
存储 算法 搜索推荐
探索常见数据结构:数组、链表、栈、队列、树和图
探索常见数据结构:数组、链表、栈、队列、树和图
94 64
|
7天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
48 16
|
30天前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
18 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
30天前
|
存储 算法 数据管理
数据结构与算法学习二零:二叉排序树(BST)、平衡二叉树(AVL)
这篇文章通过需求分析、代码实现和测试验证,详细介绍了二叉排序树的创建、遍历和删除操作,以及二叉平衡树(AVL)的自平衡特性和单旋转操作,旨在提高树结构在数据管理中的效率和性能。
24 0
数据结构与算法学习二零:二叉排序树(BST)、平衡二叉树(AVL)
|
1月前
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解(三)
【高阶数据结构】二叉树进阶探秘:AVL树的平衡机制与实现详解
|
27天前
|
Java C++
【数据结构】探索红黑树的奥秘:自平衡原理图解及与二叉查找树的比较
本文深入解析红黑树的自平衡原理,介绍其五大原则,并通过图解和代码示例展示其内部机制。同时,对比红黑树与二叉查找树的性能差异,帮助读者更好地理解这两种数据结构的特点和应用场景。
24 0
|
30天前
|
存储 算法
数据结构与算法学习十六:树的知识、二叉树、二叉树的遍历(前序、中序、后序、层次)、二叉树的查找(前序、中序、后序、层次)、二叉树的删除
这篇文章主要介绍了树和二叉树的基础知识,包括树的存储方式、二叉树的定义、遍历方法(前序、中序、后序、层次遍历),以及二叉树的查找和删除操作。
22 0
|
1月前
05(数据结构考研)树相关操作代码
05(数据结构考研)树相关操作代码
27 0
|
1月前
|
存储 算法 Java
数据结构和算法--分段树
数据结构和算法--分段树
14 0
下一篇
无影云桌面