数据结构进阶 AVL树

简介: 数据结构进阶 AVL树

AVL树的概念


首先抛出一个问题 为什么AVL树会诞生呢?


还记不记得我们在学习二叉搜索树最后总结的时候说的一句话


在二叉搜索树是完全二叉树的情况下效率最高 此时的效率是 LogN


在二叉搜索时是单边树的情况下效率最差 此时效率退化至 N


为了解决这个退化的效率问题 AVL树诞生了


AVL树是由两位俄国科学家G.M. Adelson-Velsky和E.M.Landis在1962年发明的。他们在一篇论文中提出了AVL树这一概念,并将其命名为"AVL树",以纪念他们的姓氏的首字母。

AVL树是一种自平衡二叉搜索树,它能够保证插入、删除和查找操作的时间复杂度都是O(log n),因此在很多场合下都很有用。


AVL树可以是一颗空树 也可以是具有以下性质的一颗二叉搜索树


1 它的左右子树都是AVL树

2 树的左右子树高度差不超过一 (以下样例均为右子树减左子树)

fc73aab461a744ea8803fe8d5bfdb131.png

上面就是一棵AVL树


蓝色数字表示的是左右子树的高度差值(以下简称平衡因子) 它们都是不超过1的


红色的数字表示储存数据的值 符合二叉搜索树的性质


我们可以发现 当一颗二叉搜索树是AVL树的时候 假设它一共有N个节点 那么它的高度非常接近LogN 同样的搜索效率也接近LogN


这也就解决了单边树的低效问题


AVL树节点类的定义


我们这里使用KV模型来实现AVL树


为了方便后续的操作 使用三叉链结构来完成它


3285cfe1e96748f5be11f753b8503a65.png


一个节点除了指向它的左右两个子节点之外还指向它的父节点 一共三个指针 所以称为三叉链


此外 我们还增加一个int类型的数据 来记录平衡因子


代码表示如下


template<class K, class V>
struct AVLTreeNode
{
  // 三叉连
  AVLTreeNode<K, V>* _left;
  AVLTreeNode<K, V>* _right;
  AVLTreeNode<K, V>* _parent;
  // 平衡因子
  int _bf;
  // 储存的键值对
  pair<K, V> _kv;
  // 构造函数
  AVLTreeNode(const pair<K , V>& kv)
  :_left(nullptr)
  ,_right(nullptr)
  ,_parent(nullptr)
  ,_kv(kv)
  ,_bf(0) //因为构造出来之后没有左右子树 所以说平衡因子为0
  {}
};

AVL树的插入


AVL树的插入满足下面几个步骤


1 遵循二叉搜索树的插入原则 找到待插入位置

2 找到插入位置之后 插入节点

3 更新平衡因子 如果平衡因子不满足AVL树的条件 则使之满足


第一步第二步很简单 如果还有不明白的同学可以参考这篇博客


二叉搜索树的实现


接下来我们重点学习第三步


由于我们插入一个节点之后 只有可能是这个节点的父节点的高度受到影响


所以说我们只需要更新它的祖先节点 这也是我们为什么设计成三叉链结构的原因 - 方便找祖先节点


3aee595dcea04395af593cf6a6c5d999.png

(这个图稍微有点漏洞 我们这里假设新增的节点是6.5 前面的节点都是浮点数)


新增节点之后我们开始更新平衡因子 遵循下面的规则


1 如果新增节点在父节点的右边 则平衡因子++

2 如果新增节点在父节点的左边 则平衡因子–

aafa37f30d994deebcb14879d7a0c298.png

这里我们可以发现 如果有一个父节点的平衡因子更新成了0 则无需向上继续更新了


怎么解释这个现象呢?


我们都知道 平衡因子是根据父节点左右子树的高度来确定的


如果父节点的平衡因子被更新成了0 那么肯定就说明 原来有一边的高度是大于另一边的 而且是大于1


插入一个新的节点之后两边的高度差就被填平了


那么对于父节点的父节点来说 这边子树的高度并没有变化 所以说我们不需要向上更新了

2656b091572f432b87072c2389f67c52.png


所以说我们有具体的规则如下


如果父节点的平衡因子更新之后结果为1或者-1 则需要继续向上更新祖先节点的平衡因子

如果父节点的平衡因子更新之后结果为0 则不需要继续向上更新祖先节点的平衡因子

如果父节点的平衡因子更新之后结果为2或者-2 则需要进行旋转操作来处理异常

我们假设目前的节点是cur 它的父节点是parent


因为我们执行的是插入操作 所以说我们的节点肯定是增加的 就看增加的是父节点的左子树还是右子树


因为我们不能确定是左子树还是右子树 所以要执行判断


如果cur是parent的左节点 则平衡因子–

如果cur是parent的右节点 则平衡因子++

但是到这里还没完 我们要需要继续向上更新 执行下面的代码


cur = parent;
parent = parent->_parent;


最差的情况下 我们一直要更新到根节点


按照上面的规则 我们写出代码


bool insert(const pair <K,V>& kv)
  {
  if (_root == nullptr) // 如果AVL树为空 则直接为其根节点
  {
    _root = new Node(kv);
    return true;
  }
  // 如果不为空 则按照二叉树的插入方式 通过key值 即first来比较
  Node* cur = _root; // cur确定当前位置
  Node* parent = nullptr; // parent最后连接用 
  while (cur) // 当cur不为空的时候一直走
  {
    if (kv.first > cur->_kv.first) // 如果插入值大于当前节点 向右移
    {
    cur = cur->_right;
    parent = cur;
    }
    else if (kv.first < cur->_kv.first) // 如果插入值小于当前节点 向右移
    {
    cur = cur->_left;
    parent = cur;
    }
    else // 到这里这种情况说明等于 则插入失败 返回false
    {
    return false;
    }
  }
  // 将待插入节点插入到树中
  cur = new Node(kv);
  // 根据kv的key值来分辨插入的是左子树还是右子树
  if (kv.first > parent->_kv.first)
  {
    parent->_right = cur;
    cur->_parent = parent; // 因为是三叉链 所以要更新父节点
  }
  else // 因为走到这里了kv的key值不可能相等 所以说else就表示小于
  {
    parent->_left = cur;
    cur->_parent = parent; // 因为是三叉链 所以要更新父节点
  }
  // 更新平衡因子
  while (cur != _root) // 最坏的情况 一直更新到根节点 但是cur不可能到根节点
  {
    // 如果是右则平衡因子++ 反之则--
    if (cur = parent->_left)
    {
    parent->_bf--;
    }
    else if (cur = parent->_right)
    {
    parent->_bf++;
    }
    // 平衡因子更新完毕 此时开始查看父亲节点的平衡因子是多少 是否结束 是否需要旋转
    if (parent->_bf == 0) // 如果父亲的平衡因子是0 则停止更新 直接返回
    {
    return true;
    }
    else if (parent->_bf == 1 || parent->_bf == -1) // 如果父亲的平衡因子是1或-1 则继续往上更新
    {
    cur = parent;
    parent = parent->_parent;
    }
    else if (parent->_bf == 2 || parent->_bf == -2) // 如果父亲的平衡因子是2或-2 则进行旋转操作
    {
    // 旋转
    }
  }
  }


省略了旋转操作的插入代码就在上面 接下来我们重点研究下AVL的旋转操作


AVL树的旋转


首先我们要明确一点 当parent节点的平衡因子是2或者-2时候 cur的平衡因子必然是1或者-1


因为如果cur的平衡因子是0 有两种情况


cur为新增节点 这个时候由于我们插入的是AVL树 所以其父节点只有可能是1 -1 或者0

cur为更新后的节点 此时cur为其子节点的父节点 其平衡因子更新到了0 不会继续向上更新 所以cur的父节点一定是有正常的平衡因子 1 -1 或者0

所以说我们这里就有四种情况


parent的平衡因子是2 cur的平衡因子是1

parent的平衡因子是2 cur的平衡因子是-1

parent的平衡因子是-2 cur的平衡因子是1

parent的平衡因子是-2 cur的平衡因子是-1

然后我们根据这四种情况来讨论如何旋转


左单旋

34e95ebbb107403d97407d77e58bfef5.png

从上图我们可以观察发现 parent的平衡因子被更新为了2 此时肯定要触发旋转


旋转后的图片大概是这样子


b3bc5e3874514ecdac660de0a4446d20.png


经过旋转后 平衡因子全部恢复正常 并且此时将这颗二叉树作为一个整体 它的高度还是h+2


所以不用继续往上更新平衡因子了


那么我们具体来看看实现左单旋要分几步


1 让 subr 左子树 subrl 成为 parent 的右子树

2 让 parent 成为 subr 的左子树

3 让 subr 作为整个子树的根 (如果parent不是根节点的话)

4 更新平衡因子


此时 subr 和 parent 的平衡因子都更新为0


而由于二叉搜索树的性质 所有的节点是从小到大排列的 我们这个旋转并没有改变这个性质 所以说这个旋转是完全合法的


此外 观察第一张图我们可以知道


左单旋对应着 parent的平衡因子是2 cur的平衡因子是1 这种情况


我们左单旋的代码表示如下


void RotateL(Node* parent)
  {
  Node* subR = parent->_right;
  Node* subRL = subR->_left;
  Node* grandparent = parent->_parent;
  // 建立subR和parent的联系
  subR->_left = parent;
  parent->_parent = subR;
  // 建立parent和subRL的联系
  parent->_right = subRL;
  if (subRL) // 有可能subRL为空
  {
    subRL->_parent = parent;
  }
  // 建立grandparent和sunR的联系
  // 这里要分grandparent是否存在以及parent是grandparent的左还是右
  if (grandparent) // 如果进去了 则说明grandparent存在 
  {
    // 如果存在 要分清楚parent在左边还是右边
    if (grandparent->_left == parent)
    {
    grandparent->_left = subR;
    }
    else
    {
    grandparent->_right = subR;
    }
    subR->_parent = grandparent;
  }
  // 否则就是不存在 我们更新_root 并且将subR的父节点置空
  else
  {
    _root = subR;
    subR->_parent = nullptr;
  }
  // 更新平衡因子
  subR->_bf = 0;
  parent->_bf = 0;
  }


只要按照上面的步骤一行行写 应该是不难理解的


这里尤其要注意的是 我们使用的是不熟悉的三叉链结构


右单旋

c897b8d8e2014c75b4e7a6734f86a1fb.png

从上图我们可以观察发现 parent的平衡因子被更新为了-2 此时肯定要触发旋转


旋转后的图片大概是这样子


ec8752d88e564e2c848f61553a6f621f.png

经过旋转后 平衡因子全部恢复正常 并且此时将这颗二叉树作为一个整体 它的高度还是h+2


所以不用继续往上更新平衡因子了


那么我们具体来看看实现右单旋要分几步


1 让 subl 的右子树 sublr 成为 parent 的左子树

2 让 parent 成为 subl 的左子树

3 让 subr 作为整个子树的根 (如果parent不是根节点的话)

4 更新平衡因子


此时 subl 和 parent 的平衡因子都更新为0


而由于二叉搜索树的性质 所有的节点是从小到大排列的 我们这个旋转并没有改变这个性质 所以说这个旋转是完全合法的


此外 观察第一张图我们可以知道


右单旋对应着 parent 的平衡因子是-2 cur 的平衡因子是-1 这种情况


我们右单旋的代码表示如下


void RotateR(Node* parent)
  {
  Node* subL = parent->_left;
  Node* subLR = subL->_right;
  Node* grandparent = parent->_parent;
  // 建立subL和parent之间的联系
  subL->_right = parent;
  parent->_parent = subL;
  // 建立parent和subLR之间的联系 
  parent->_left = subLR;
  if (subLR) // subLR可能不存在 
  {
    subLR->_parent = parent;
  }
  // 建立grandparent和subl之间的联系
  // 这里要分grandparent是否存在以及parent是grandparent的左还是右
  if (grandparent)
  {
    // 如果存在 要分清楚parent在左边还是右边
    if (grandparent->_left == parent)
    {
    grandparent->_left = subL;
    }
    else
    {
    grandparent->_right = subL;
    }
    subL->_parent = grandparent;
  }
  else // 这里就是表示parent就是根节点
  {
    _root = subL;
    subL->_parent = nullptr;
  }
  // 更新平衡因子
  subL->_bf = 0;
  parent->_bf = 0;
  }


左右双旋

5d49dde92f764a1ea7fd3fc044b06e32.png


左右双旋一般分三步走 图片表示如下


第一步 插入新增节点 触发旋转机制


9b4e81de7c404ebb91d34e89eefe4a9a.png


第二步 以30为旋转点进行左单旋


6cc44983651849768c5ca2edc42e3a04.png

第三步 以90为旋转点进行右单旋


89246f809f6046568971306dea39eb59.png

左右双旋的步骤如下


1 以subL为旋转点进行左单旋

2 以parent为旋转点进行右单旋

3 更新平衡因子


而由于二叉搜索树的性质 所有的节点是从小到大排列的 我们这个旋转并没有改变这个性质 所以说这个旋转是完全合法的


此外 观察第二张图我们可以知道


左右双旋对应着 parent 的平衡因子是-2 cur 的平衡因子是1 这种情况


左右双旋后 平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况


1 当sublr的平衡因子是-1时 左右双旋后parent subl sublr的平衡因子分别更新为 1 0 0

2 当sublr的平衡因子是 1时 左右双旋后parent subl sublr的平衡因子分别更新为 0 -1 0

3 当sublr的平衡因子是 0时 左右双旋后parent subl sublr的平衡因子分别更新为 0 0 0


产生这三种情况的原因分别是


插入的节点可能是sublr 的左子树 右子树 或者是sublr本身


代码表示如下


void RotateLR(Node* parent)
  {
  Node* subL = parent->_left;
  Node* subLR = parent->_left->_right;
  int bf = subLR->_bf;
  // 以subL作为旋转点左单旋
  RotateL(subL);
  // 以parent作为旋转点右单旋
  RotateR(parent);
  // 平衡因子
  if (bf == 1)
  {
    subLR->_bf = 0;
    subL->_bf = -1;
    parent->_bf = 0;
  }
  else if (bf == -1)
  {
    subLR->_bf = 0;
    subL->_bf = 0;
    parent->_bf = 1;
  }
  else if (bf == 0)
  {
    subLR->_bf = 0;
    subL->_bf = 0;
    parent->_bf = 0;
  }
  else
  {
    assert(false); 
  }
  }


右左单旋


84d74471af504edd87b526f665b1190f.png

右左双旋一般分三步走 图片表示如下


第一步 插入新增节点 触发旋转机制

1fa1f0ccb7c841fc97601953990f5c84.png



第二步 以90作为节点进行右单旋


83ef99fbb4d046d2a2164c71132d29dc.png


第三步 以30作为节点进行左单旋


f9f58b5bc5c548e18c9fae797df45437.png


右左双旋的步骤如下


1 以subR为旋转点进行右单旋

2 以parent为旋转点进行左单旋

3 更新平衡因子


而由于二叉搜索树的性质 所有的节点是从小到大排列的 我们这个旋转并没有改变这个性质 所以说这个旋转是完全合法的


此外 观察第二张图我们可以知道


右左双旋对应着 parent 的平衡因子是2 cur 的平衡因子是-1 这种情况


右左双旋后 平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况


1 当subrl的平衡因子是-1时 左右双旋后parent subr subrl的平衡因子分别更新为 1 0 0

2 当subrl的平衡因子是-1时 左右双旋后parent subr subrl的平衡因子分别更新为 0 -1

3 当subrl的平衡因子是-1时 左右双旋后parent subr subrl的平衡因子分别更新为 0 0 0


产生这三种情况的原因分别是


插入的节点可能是subrl 的左子树 右子树 或者是subrl本身


代码表示如下


void RotateRL(Node* parent)
  {
  Node* subR = parent->_right;
  Node* subRL = subR->_left;
  int bf = subRL->_bf;
  //  以subR为轴进行右单旋
  RotateR(subR);
  //  以parent为轴进行左单旋
  RotateL(parent);
  //  更新平衡因子
  if (bf == 1)
  {
    subRL->_bf = 0;
    parent->_bf = -1;
    subR->_bf = 0;
  }
  else if (bf == -1)
  {
    subRL->_bf = 0;
    parent->_bf = 0;
    subR->_bf = 1;
  }
  else if (bf == 0)
  {
    subRL->_bf = 0;
    parent->_bf = 0;
    subR->_bf = 0;
  }
  else
  {
    assert(false); 
  }
  }

AVL树的验证


首先AVL树是一颗二叉搜索树 所以说它满足中序遍历后是有序的这个原则 我们可以使用中序遍历来验证


代码表示如下

void _Inorder(Node* root)
  {
  if (root == nullptr)
  {
    return;
  }
  _Inorder(root->_left);
  cout << root->_kv.first << " ";
  _Inorder(root->_right);
  }
  void Inorder()
  {
  _Inorder(_root);
  }


但是中序遍历只能证明这棵树是二叉搜索树 要证明它是AVL树我们必须还要证明它的平衡因子全部无异常


我们可以从根节点开始 往上遍历每个子树来判断它是否是AVL树 如果全部是AVL树 则可以验证


如果有一颗不是 则证明不是AVL树

//判断是否为AVL树
bool IsAVLTree()
{
  int hight = 0; //输出型参数
  return _IsBalanced(_root, hight);
}
//检测二叉树是否平衡
bool _IsBalanced(Node* root, int& hight)
{
  if (root == nullptr) //空树是平衡二叉树
  {
  hight = 0; //空树的高度为0
  return true;
  }
  //先判断左子树
  int leftHight = 0;
  if (_IsBalanced(root->_left, leftHight) == false)
  return false;
  //再判断右子树
  int rightHight = 0;
  if (_IsBalanced(root->_right, rightHight) == false)
  return false;
  //检查该结点的平衡因子
  if (rightHight - leftHight != root->_bf)
  {
  cout << "平衡因子设置异常:" << root->_kv.first << endl;
  }
  //把左右子树的高度中的较大值+1作为当前树的高度返回给上一层
  hight = max(leftHight, rightHight) + 1;
  return abs(rightHight - leftHight) < 2; //平衡二叉树的条件
}


AVL树的查找


AVL树的查找方式与二叉搜索树相同


1 如果树为空树 则查找失败 返回nullptr

2 如果key值小于当前节点 则进入当前节点的子树查找

3 如果key值大于当前节点 则进入当前节点的子树查找

4 如果key值等于当前节点 则返回当前节点

代码表示如下


Node* find(const K& key)
  {
  Node* cur = _root;
  while (cur) // 一直查找到空为止
  {
    if (key < cur->_kv.first) // 小于
    {
    cur = cur->_left;
    }
    else if (key < cur->_kv.first) // 大于
    {
    cur = cur->_right;
    }
    else // 等于
    {
    return cur
    }
  }
  // 走到这里就是没找到
  return nullptr;
  }


AVL树的修改


我们所说的修改一般是值对于value值的修改


一般步骤如下


1 找到对应的key值的结点

2 对其value值进行修改


代码表示如下

bool Modify(const K& key, const V& value)
  {
  Node* ret = find(key);
  // 有可能不存在
  if (ret == nullptr)
  {
    return false;
  }
  ret->_kv.second = value;
  return true;
  }


AVL树的删除


AVL树的删除满足下面几个步骤


1遵循二叉搜索树的删除原则 找到待删除位置

2找到删除位置之后 删除节点

3更新平衡因子 如果平衡因子不满足AVL树的条件 则使之满足


由于我们学习AVL树主要是为了后面的红黑树做铺垫 需要重点掌握的是AVL树的插入


并且AVL树的删除在平时的笔试面试中考察的很少 或者说几乎没有考察 所以同学们可以跳过这一部分的学习


AVL树的性能


AVL树是一颗近乎完全二叉树的二叉搜索树 所以说它查询的时间复杂度非常低 为Log N


但是如果要对于AVL树做一些结构修改的操作 它的性能就会十分低下 比如在插入和删除时 可能要经过大量的旋转操作


因此 如果是需要一种查询高效的数据结构 AVL树是十分合适的

但是 如果它经常要被修改 AVL树的性能就可能不太匹配了

相关文章
|
3天前
|
数据可视化 数据挖掘 数据处理
【Python进阶(七)】——Series数据结构
【Python进阶(七)】——Series数据结构
|
5天前
|
存储 算法 C++
数据结构/C++:AVL树
数据结构/C++:AVL树
8 2
|
5天前
|
JSON 数据可视化 Shell
数据结构可视化 Graphviz在Python中的使用 [树的可视化]
数据结构可视化 Graphviz在Python中的使用 [树的可视化]
10 0
|
5天前
|
存储 缓存 算法
数据结构与算法 树(B树,B+树,红黑树待完善)
数据结构与算法 树(B树,B+树,红黑树待完善)
11 0
|
8天前
|
存储 分布式数据库
【数据结构】树和二叉树堆(基本概念介绍)
【数据结构】树和二叉树堆(基本概念介绍)
23 6
|
14天前
|
存储
数据结构第五课 -----线性表之树
数据结构第五课 -----线性表之树
|
15天前
|
Java
数据结构奇妙旅程之二叉平衡树进阶---AVL树
数据结构奇妙旅程之二叉平衡树进阶---AVL树
|
7天前
|
机器学习/深度学习 算法 测试技术
【单调栈】3113. 边界元素是最大值的子数组数目
【单调栈】3113. 边界元素是最大值的子数组数目
|
1天前
|
存储 NoSQL C语言
数据结构——顺序栈与链式栈的实现-2
数据结构——顺序栈与链式栈的实现
数据结构——顺序栈与链式栈的实现-2