数据结构——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结点,那就是替换法。

相关文章
|
12月前
|
算法
数据结构之博弈树搜索(深度优先搜索)
本文介绍了使用深度优先搜索(DFS)算法在二叉树中执行遍历及构建链表的过程。首先定义了二叉树节点`TreeNode`和链表节点`ListNode`的结构体。通过递归函数`dfs`实现了二叉树的深度优先遍历,按预序(根、左、右)输出节点值。接着,通过`buildLinkedList`函数根据DFS遍历的顺序构建了一个单链表,展示了如何将树结构转换为线性结构。最后,讨论了此算法的优点,如实现简单和内存效率高,同时也指出了潜在的内存管理问题,并分析了算法的时间复杂度。
320 0
|
8月前
|
算法 Java
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
205 3
 算法系列之数据结构-Huffman树
|
8月前
|
存储 自然语言处理 数据库
【数据结构进阶】AVL树深度剖析 + 实现(附源码)
在深入探讨了AVL树的原理和实现后,我们不难发现,这种数据结构不仅优雅地解决了传统二叉搜索树可能面临的性能退化问题,还通过其独特的平衡机制,确保了在任何情况下都能提供稳定且高效的查找、插入和删除操作。
632 19
|
10月前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
319 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
10月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
271 12
|
10月前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
187 10
|
10月前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
395 3
|
12月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
348 5
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
530 16
|
12月前
|
算法
数据结构之文件系统模拟(树数据结构)
本文介绍了文件系统模拟及其核心概念,包括树状数据结构、节点结构、文件系统类和相关操作。通过构建虚拟环境,模拟文件的创建、删除、移动、搜索等操作,展示了文件系统的基本功能和性能。代码示例演示了这些操作的具体实现,包括文件和目录的创建、移动和删除。文章还讨论了该算法的优势和局限性,如灵活性高但节点移除效率低等问题。
240 0

热门文章

最新文章