数据结构(九):树拓展(平衡二叉搜索树),AVL树节点封装

441 阅读5分钟

平衡树

平衡树是一种特殊的二叉搜索树,其目的是通过一些特殊的技巧来维护树的高度平衡,从而保证树的插入、搜索、删除等操作的时间复杂度都较低(趋近O(logn))。
那么为什么二叉搜索树需要平衡呢?因为当一棵二叉搜索树插入或删除数据的顺序高度一致时,二叉搜索树就会退化成接近链表的状态(比如一棵数据从小到大插入的二叉搜索树)。这时树的插入、删除、搜索的时间复杂度都会变成O(n)。因此,当我们需要高效的处理大量数据时,搜索树的平衡就变得至关重要。

常见的平衡二叉搜索树

  • AVL树:一种最早的平衡二叉搜索树
  • 红黑树:一种比较流行的平衡二叉搜索树,由R.Bayer在1972年发明
  • Splay树:一种动态平衡二叉搜索树
  • Treap:一种随机化的平衡二叉搜索树,是二叉搜索树和堆的结合
  • B-树:一种适用于磁盘或其他外存存储设备的多路平衡查找树

AVL树和红黑树是应用最广泛的两种平衡二叉搜索树,由于红黑树过于复杂,所以我们本章节主要讲解AVL树。

AVL树

AVL树是一种自平衡的二叉搜索树,它通过旋转操作保证树的平衡。那么AVL树是如何判断一棵树到底平不平衡呢?
在AVL树中,每个节点都有一个平衡因子,该值代表了以该节点为根节点的子树的高度差。该高度差的绝对值不能大于1,所以当节点平衡因子的绝对值超过1时,我们就要通过旋转来重新平衡树。下面是一幅标记各个节点平衡因子AVL树:

image.png 对于AVL树的封装,为了方便读者理解,将分为以下几步:

  1. AVL树节点封装
  2. AVL旋转操作(左旋、右旋)
  3. 不同情况调用不同旋转方法
  4. 插入操作
  5. 删除操作

AVL树节点封装

初始化属性和一些基础的方法(由于比较简单直接给出):

class AVLTreeNode<T> {
  value: T;
  left: AVLTreeNode<T> | null = null;
  right: AVLTreeNode<T> | null = null;
  parent: AVLTreeNode<T> | null = null;
  constructor(value: T) {
    this.value = value;
  }
  // 高度
  get height(): number {
    return Math.max(this.left?.height || 0, this.right?.height || 0) + 1;
  }
  // 平衡因子 = 左子树高度 - 右子树高度
  get balanceFactor(): number {
    return (this.left?.height || 0) - (this.right?.height || 0);
  }
  // 是否是平衡节点
  get isBalanced(): boolean {
    return Math.abs(this.balanceFactor) <= 1;
  }
  // 获取子节点中高度较高的节点 该节点就是要旋转的轴心节点
  get higherChild(): AVLTreeNode<T> | null {
    const leftHeight = this.left?.height || 0;
    const rightHeight = this.right?.height || 0;
    // 如果左右子树高度相等且当前节点是父节点的左子节点,返回左子节点,否则返回右子节点
    if (leftHeight === rightHeight) return this.isLeftChild ? this.left : this.right;
    // 如果左子树高度大于右子树高度,返回左子节点,否则返回右子节点
    return leftHeight > rightHeight ? this.left : this.right;
  }
  // 是否是父节点的左子节点
  get isLeftChild() {
    return this.parent !== null && this.parent.left === this;
  }
  // 是否是父节点的右子节点
  get isRightChild() {
    return this.parent !== null && this.parent.right === this;
  }
}

这里需要解释的是为什么higherChild获取的节点是轴心节点,试想如下情况,依次插入5、3、2节点:

image.png 此时会发现这棵二叉搜索树已经不平衡了,不平衡的节点就是5,此时AVL树要做的旋转操作就是以3为轴心进行右旋转,此时higherChild的值正是3节点。

右旋转(左左情况)

对于上述情况,由于是最后插入的2造成5节点的不平衡,而2是5左子树的左节点,我们称这种情况为左左不平衡(left left),这种情况我们要进行的操作就是右旋转。那么我们要怎么旋转呢?我们参考更为复杂一点的情况来看:

image.png

  1. 确定轴心:为不平衡节点的左子节点(pivot:3节点)
  2. 处理轴心的右子节点(图中的B):pivot的右子节点要挂载到root节点的left,同时要指定B的父节点为root
  3. 处理pivot节点:要把root挂载到pivot的right,如果root有父节点,那么判断root是左子节点还是右子节点,将pivot挂载到root父节点的left或right,然后pivot的父节点赋值为this的父节点。
  4. 将root的父节点赋值为pivot,判断如果pivot没有父节点,返回pivot,将pivot作为整棵树的根节点。

做完上述四步,树就会变成这样:

image.png 那么具体转换成代码实现:

// 右旋转
rotateRight() {
  // 1、获取轴心节点
  const pivot = this.higherChild!;
  // 2、处理pivot的右子节点
  this.left = pivot.right;
  if (pivot.right) {
    pivot.right.parent = this;
  }
  // 3、处理pivot节点
  pivot.right = this;
  pivot.parent = this.parent;
  if (this.parent) {
    if (this.isLeftChild) {
      this.parent.left = pivot;
    } else {
      this.parent.right = pivot;
    }
  }
  // 4、处理root节点(this)
  this.parent = pivot;
  // 如果pivot是根节点,返回pivot
  if (!pivot.parent) return pivot;
  // 默认返回null 意味着不需要更新根节点
  return null;
}
左旋转(右右情况)

理解了左左情况的右旋转,那么右右情况的左旋转也是类似的。右右情况:

image.png
左旋转的步骤和右旋转基本相同,这里直接给出实现:

// 左旋转
rotateLeft() {
  // 1、获取轴心节点
  const pivot = this.higherChild!;
  // 2、处理pivot的左子节点
  this.right = pivot.left;
  if (pivot.left) {
    pivot.left.parent = this;
  }
  // 3、处理pivot节点
  pivot.left = this;
  pivot.parent = this.parent;
  if (this.parent) {
    if (this.isLeftChild) {
      this.parent.left = pivot;
    } else {
      this.parent.right = pivot;
    }
  }
  // 4、处理root节点(this)
  this.parent = pivot;
  // 如果pivot是根节点,返回pivot
  if (!pivot.parent) return pivot;
  // 默认返回null 意味着不需要更新根节点
  return null;
}
旋转方法合并

可以看到两种旋转的实现高度相似,所以可以进行合并:

// 左右旋转封装 传入旋转方向
rotateByDir(direction: 'left' | 'right') {
  // 1、获取轴心节点
  const pivot = this.higherChild!;
  // 2、处理pivot的子节点
  this[direction === 'left' ? 'right' : 'left'] = pivot[direction];
  if (pivot[direction]) {
    pivot[direction]!.parent = this;
  }
  // 3、处理pivot节点
  pivot[direction] = this;
  pivot.parent = this.parent;
  if (this.parent) {
    if (this.isLeftChild) {
      this.parent.left = pivot;
    } else {
      this.parent.right = pivot;
    }
  }
  // 4、处理root节点(this)
  this.parent = pivot;
  // 如果pivot是根节点,返回pivot
  if (!pivot.parent) return pivot;
  // 默认返回null 意味着不需要更新根节点
  return null;
}

以上就是AVL树节点方法的封装,重点是旋转的思路要理清楚,代码实现其实并不复杂。由于剩余的内容还有很多,所以不同情况调用不同的旋转方法、插入删除操作将在下一篇文章讲解,欢迎订阅本系列。