AVL树是一种自平衡树。添加或移除节点时, AVL树会尝试保持自平衡。任意一个节点(不论深度)的左子树和右子树高度最多相差 1。添加或移除节点时, AVL树会尽可能尝试转换为完全树。从创建我们的 AVLTree 类开始,声明如下。

  1. class AVLTree extends BinarySearchTree {
  2. constructor(compareFn = defaultCompare) {
  3. super(compareFn);
  4. this.compareFn = compareFn;
  5. this.root = null;
  6. }
  7. }

既然 AVL 树是一个 BST,我们可以扩展我们写的 BST 类,只需要覆盖用来维持 AVL 树平衡的方法,也就是 insert、 insertNode 和 removeNode 方法。所有其他的 BST 方法将会被 AVLTree 类继承。

在 AVL 树中插入或移除节点和 BST 完全相同。然而, AVL 树的不同之处在于我们需要检验它的平衡因子,如果有需要,会将其逻辑应用于树的自平衡。

我们将会学习怎样创建 remove 和 insert 方法,但是首先需要学习 AVL 树的术语和它的旋转操作。

1. 节点的高度和平衡因子


正如本章开头所述,节点的高度是从节点到其任意子节点的边的最大值。下图展示了一个包含每个节点高度的树。
image.png
计算一个节点高度的代码如下。

getNodeHeight(node) {
  if (node == null) {
    return -1;
  }
  return (
    Math.max(this.getNodeHeight(node.left), this.getNodeHeight(node.right)) + 1
  );
}

在 AVL 树中,需要对每个节点计算右子树高度( hr)和左子树高度( hl)之间的差值,该
值( hr- hl)应为 0、 1 或1。如果结果不是这三个值之一,则需要平衡该 AVL 树。这就是平衡
因子的概念。

下图举例说明了一些树的平衡因子(所有的树都是平衡的)。

image.png
遵循计算一个节点的平衡因子并返回其值的代码如下。

getBalanceFactor(node) {
  const heightDifference =
    this.getNodeHeight(node.left) - this.getNodeHeight(node.right);
  switch (heightDifference) {
    case -2:
      return BalanceFactor.UNBALANCED_RIGHT;
    case -1:
      return BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT;
    case 1:
      return BalanceFactor.SLIGHTLY_UNBALANCED_LEFT;
    case 2:
      return BalanceFactor.UNBALANCED_LEFT;
    default:
      return BalanceFactor.BALANCED;
  }
}

为了避免直接在代码中处理平衡因子的数值,我们还要创建一个用来作为计数器的 JavaScript 常量。

const BalanceFactor = {
  UNBALANCED_RIGHT: 1,
  SLIGHTLY_UNBALANCED_RIGHT: 2,
  BALANCED: 3,
  SLIGHTLY_UNBALANCED_LEFT: 4,
  UNBALANCED_LEFT: 5,
};

我们会在下面学习到每个 heightDifference 表示什么。

2. 平衡操作——AVL 旋转

在对 AVL 树添加或移除节点后,我们要计算节点的高度并验证树是否需要进行平衡。向 AVL 树插入节点时,可以执行单旋转或双旋转两种平衡操作,分别对应四种场景。

左-左( LL):向右的单旋转
右-右( RR):向左的单旋转
左-右( LR):向右的双旋转(先 RR 旋转,再 LL 旋转)
右-左( RL):向左的双旋转(先 LL 旋转,再 RR 旋转)

  • 左-左( LL):向右的单旋转
    这种情况出现于节点的左侧子节点的高度大于右侧子节点的高度时,并且左侧子节点也是平衡或左侧较重的,如下图所示。

image.png
我们来看一个实际的例子,如下图所示。
image.png

假设向 AVL 树插入节点 5,这会造成树失衡(节点 50-Y 高度为+2),需要恢复树的平衡。下面是我们执行的操作:
(1)与平衡操作相关的节点有三个( X、 Y、 Z),将节点 X 置于节点 Y(平衡因子为+2)所在
的位置(行 {1});
(2)节点 X 的左子树保持不变
(3)将节点 Y 的左子节点置为节点 X 的右子节点(行 {2});
(4)将节点 X 的右子节点置为节点 Y(行 {3})。

下面的代码举例说明了整个过程。

rotationLL(node) {
  const tmp = node.left; // {1}
  node.left = tmp.right; // {2}
  tmp.right = node; // {3}
  return tmp;
}
  • 右-右( RR):向左的单旋转

右-右的情况和左-左的情况相反。它出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点也是平衡或右侧较重的,如下图所示。
image.png
我们来看一个实际的例子,如下图所示。

image.png
假设向 AVL 树插入节点 90,这会造成树失衡(节点 50-Y 高度为2),因此需要恢复树的平衡。下面是我们执行的操作:
(1)与平衡操作相关的节点有三个( X、 Y、 Z),将节点 X 置于节点 Y(平衡因子为2)所在
的位置(行{1});
(2)节点 X 的右子树保持不变;
(3)将节点 Y 的右子节点置为节点 X 的左子节点(行{2});
(4)将节点 X 的左子节点置为节点 Y(行{3})。

下面的代码举例说明了整个过程。

rotationRR(node) {
  const tmp = node.right; // {1}
  node.right = tmp.left; // {2}
  tmp.left = node; // {3}
  return tmp;
}
  • 左-右(LR):向右的双旋转.

这种情况出现于左侧子节点的高度大于右侧子节点的高度,并且左侧子节点右侧较重。在这种情况下,我们可以对左侧子节点进行左旋转来修复,这样会形成左-左的情况,然后再对不平衡的节点进行一个右旋转来修复,如下图所示。
image.png
我们来看一个实际的例子,如下图所示

1634218122.png
假设向 AVL 树插入节点 35,这会造成树失衡(节点 50-Y 高度为+2),需要恢复树的平衡。下面是我们执行的操作:
(1)将节点 X 置于节点 Y(平衡因子为+2)所在的位置;
(2)将节点 Y 的左子节点置为节点 X 的右子节点;
(3)将节点 Z 的右子节点置为节点 X 的左子节点;
(4)将节点 X 的右子节点置为节点 Y;
(5)将节点 X 的右子节点置为节点 Z。

基本上,就是先做一次 RR 旋转(左旋),再做一次 LL 旋转(右旋)。

下面的代码举例说明了整个过程。

rotationLR(node) {
  node.left = this.rotationRR(node.left);
  return this.rotationLL(node);
}
  • 右-左(RL):向左的双旋转

右-左的情况和左-右的情况相反。这种情况出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点左侧较重。在这种情况下我们可以对右侧子节点进行右旋转来修复,这样会形成右右的情况,然后我们再对不平衡的节点进行一个左旋转来修复,如下图所示。
image.png
我们来看一个实际的例子,如下图所示。

1634218275.png
假设向 AVL 树插入节点 75,这会造成树失衡(节点 70-Y 高度为2),需要恢复树的平衡。
下面是我们执行的操作:
(1)将节点 X 置于节点 Y(平衡因子为2)所在的位置;
(2)将节点 Z 的左子节点置为节点 X 的右子节点;
(3)将节点 Y 的右子节点置为节点 X 的左子节点 ;
(4)将节点 X 的左子节点置为节点 Y ;
(5)将节点 X 的右子节点置为节点 Z。

基本上,就是先做一次 LL 旋转(右旋),再做一次 RR 旋转(左旋)。

下面的代码举例说明了整个过程。

rotationRL(node) {
    node.right = this.rotationLL(node.right);
    return this.rotationRR(node);
}

理解了这些概念,我们就可以专注于向树添加阶段和从树移除节点的代码了。

3. 向 AVL 树插入节点

向 AVL 树插入节点和在 BST 中是一样的。除了插入节点外,我们还要验证插入后树是否还是平衡的,如果不是,就要进行必要的旋转操作。

下面的代码向 AVL 树插入了一个新节点。

function insert(key) {
  this.root = this.insertNode(this.root, key);
}

function insertNode(node, key) {
  // 像在 BST 树中一样插入节点
  if (node == null) {
    return new Node(key);
  } else if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
    node.left = this.insertNode(node.left, key);
  } else if (this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
    node.right = this.insertNode(node.right, key);
  } else {
    return node; // 重复的键
  }
  // 如果需要,将树进行平衡操作
  const balanceFactor = this.getBalanceFactor(node); // {1}
  if (balanceFactor === BalanceFactor.UNBALANCED_LEFT) {
    // {2}
    if (this.compareFn(key, node.left.key) === Compare.LESS_THAN) {
      // {3}
      node = this.rotationLL(node); // {4}
    } else {
      return this.rotationLR(node); // {5}
    }
  }
  if (balanceFactor === BalanceFactor.UNBALANCED_RIGHT) {
    // {6}
    if (this.compareFn(key, node.right.key) === Compare.BIGGER_THAN) {
      // {7}
      node = this.rotationRR(node); // {8}
    } else {
      return this.rotationRL(node); // {9}
    }
  }
  return node;
}

在向 AVL 树插入节点后,我们需要检查树是否需要进行平衡,因此要使用递归计算以每个插入树的节点为根的节点的平衡因子(行{1}),然后对每种情况应用正确的旋转。

如果在向左侧子树插入节点后树不平衡了(行{2}),我们需要比较是否插入的键小于左侧子节点的键(行{3})。如果是,我们要进行 LL 旋转(行{4})。否则,要进行 LR 旋转(行{5})。

如果在向右侧子树插入节点后树不平衡了(行{6}),我们需要比较是否插入的键小于右侧子节点的键(行{7})。如果是,我们要进行 RR 旋转(行{8})。否则,要进行 RL 旋转(行{9})。 

4. 从 AVL 树中移除节点

从 AVL 树移除节点和在 BST 中是一样的。除了移除节点外,我们还要验证移除后树是否还是平衡的,如果不是,就要进行必要的旋转操作。

下面的代码从 AVL 树移除了一个节点。

function removeNode(node, key) {
  node = super.removeNode(node, key); // {1}
  if (node == null) {
    return node; // null,不需要进行平衡
  }
  // 检测树是否平衡
  const balanceFactor = this.getBalanceFactor(node); // {2}
  if (balanceFactor === BalanceFactor.UNBALANCED_LEFT) {
    // {3}
    const balanceFactorLeft = this.getBalanceFactor(node.left); // {4}
    if (
      balanceFactorLeft === BalanceFactor.BALANCED ||
      balanceFactorLeft === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT
    ) {
      // {5}
      return this.rotationLL(node); // {6}
    }
    if (balanceFactorLeft === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT) {
      // {7}
      return this.rotationLR(node.left); // {8}
    }
  }
  if (balanceFactor === BalanceFactor.UNBALANCED_RIGHT) {
    // {9}
    const balanceFactorRight = this.getBalanceFactor(node.right); // {10}
    if (
      balanceFactorRight === BalanceFactor.BALANCED ||
      balanceFactorRight === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT
    ) {
      // {11}
      return this.rotationRR(node); // {12}
    }
    if (balanceFactorRight === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT) {
      // {13}
      return this.rotationRL(node.right); // {14}
    }
  }
  return node;
}


既然 AVLTree 类是 BinarySearchTree 类的子类,我们也可以使用 BST 的 removeNode方法来从 AVL 树中移除节点(行{1})。在从 AVL 树中移除节点后,我们需要检查树是否需要进行平衡,所以使用递归计算以每个移除的节点为根的节点的平衡因子(行{2}),然后需要对每种情况应用正确的旋转。

如果在从左侧子树移除节点后树不平衡了(行{3}),我们要计算左侧子树的平衡因子(行{4})。如果左侧子树向左不平衡(行{5}),要进行 LL 旋转(行{6});如果左侧子树向右不平衡(行{7}),要进行 LR 旋转(行{8})。

最后一种情况是,如果在从右侧子树移除节点后树不平衡了(行{9}),我们要计算右侧子树的平衡因子(行{10})。如果右侧子树向右不平衡(行{11}),要进行 RR 旋转(行{12});如果右侧子树向左不平衡(行{13}),要进行 LR 旋转(行{14})。