平衡二叉树(AVL)
引子
上一篇我们介绍了二叉排序树,并且提到理想情况下,二叉排序树的插入、删除、查找时间复杂度都是 O(logn)
,非常高效,而且它是一种动态的数据结构,插入删除性能合查找一样好,不像之前提到的二分查找,虽然查找性能也是 O(logn)
,但是需要先对线性表进行排序,二排序的最好时间复杂度也是 O(nlogn)
,所以二分查找不适合动态结构的排序。
但是我们也提到如果二叉排序树构造的不好的话就会退化成斜树:
此时按照之前的实现算法性能退化成了 O(n)
,所以如何构造二叉排序树很重要,我们的理想情况是满二叉树和完全二叉树,它们的性能都是 O(logn)
,所以我们在构造二叉排序树的时候要尽可能像它们靠近,才能得到最佳的操作性能,由此引出了我们今天的话题——平衡二叉树。
什么是平衡二叉树
平衡二叉树的英文名是 Self-Balancing Binary Search Tree 或者 Height-Balancing Binary Search Tree,译作自平衡的二叉查找树,或者高度平衡的二叉查找树,二叉查找树和二叉排序树是一个意思,只是叫法不同,简称平衡二叉树,也叫 AVL 树(平衡二叉树作者的名字首字母),所以平衡二叉树首先是二叉排序树,并且这个二叉排序树是左右高度平衡的,这么讲有点抽象,具体来说,平衡二叉树要求每个节点的左子树和右子树的高度差至多等于 1,这个高度(深度)差的值叫做平衡因子 BF,也就是说 BF 的值不能大于1,否则就不是平衡二叉树。
我们简单看几个例子:
- 图1满足平衡二叉树的定义,是平衡二叉树;
- 图2所示二叉树不是二叉排序树,所有不是平衡二叉树;
- 图3不满足平衡因子小于等于1的要求,对58这个节点来说,平衡因子BF的值是3,因而不是平衡二叉树;
- 图4满足平衡二叉树的定义,是平衡二叉树;
我们之所以这么约束平衡二叉树,是为了保证它能够始终做到插入、删除、查找的时间复杂度是 O(logn)
。
平衡二叉树的实现原理
平衡二叉树的基本实现思路,是在构建二叉排序树的时候,每插入一个节点,都要检查这个节点的插入是否破坏了原有的平衡性,如果是的话,则找出最小不平衡子树,在保证整体二叉排序树的前提下,通过左旋或者右旋的方式将其调整为平衡子树。从而动态维护这棵平衡二叉树。
这里面有几个概念需要解释一下:
1、最小不平衡子树
距离插入节点最近的,且平衡因子绝对值大于 1 的节点为根的子树,叫做最小不平衡子树:
比如上图中以存储元素 58
的节点为根的子树叫做最小不平衡子树。
2、左旋/右旋
所谓左旋和右旋指的是最小不平衡子树旋转的方向。
如果平衡因子小于 -1
,即右子树高度值比较大,则需要左旋:
反之,如果平衡因子大于1,即左子树高度值比较大,则需要右旋:
当然为了方便你理解原理,我们这里给出的都是最简化的情况,实际处理过程中比这个更复杂,下一篇我们将具体给你演示如何通过代码在各种情况下实现平衡二叉树并讨论对应的时间复杂度。
平衡二叉树的构建实现过程演示
实例演示
在开始之前,我们先通过一个对比来加强理解,在没有介绍平衡二叉树之前,你可能会构造出这样的一棵二叉树:
虽然这也是一棵二叉排序树,但是层数达到 8,显然可以通过平衡二叉树来降低层数,提高性能,如果把它转化为平衡二叉树,会是这个样子:
层数降低了一半,变成了 4 层,显然性能要比之前要高。那么这个平衡二叉树是怎么构建的呢?假设插入节点的顺序是{3,2,1,4,5,6,7,10,9,8}
,两个节点之前不用考虑,我们从第三个节点开始分析:
插入第三个节点 1 时,左子树高度是 2,右子树高度是 0,高度差的绝对值是 2,不符合平衡二叉树的要求,需要把以 3 为根节点的子树进行右旋,到右图那个样子,左右子树高度差为 0,符合平衡二叉树要求,完成调整。同理,插入第四个节点 4 的时候,左右子树高度为 -1,符合平衡二叉树要求,继续插入第五个节点,此时又不符合平衡二叉树的要求了,这个时候右子树比较高,需要左旋:
旋转的时候以最小不平衡子树为单位,此时最小的不平衡子树是 3、4、5 节点构成的子树,我们以 4 为中心进行左旋,将树结构调整为右图所示的样子,满足了平衡二叉树的要求,停止调整。注意到我们每次新增节点的时候,会调整以每个节点为根节点的左右子树的高度差,然后从最小子树开始进行调整,直到以每个节点为根节点的子树符合平衡二叉树的要求,这样整棵树就符合平衡二叉树的要求了。
继续增加节点,当插入节点 6 时,发现根节点 2 上维护的高度差值为 -2,又不满足平衡二叉树了,这个时候,需要以 2 为中心对树进行左旋,最终调整为右图所示的结构满足平衡二叉树要求(右子树中旋转到根节点的节点对应左子树需要移到旋转后二叉树的左子树中):
继续增加节点 7,此时以 5 为根节点的最小子树不满足平衡二叉树的要求了,需要左旋:
继续增加节点 10,满足平衡二叉树要求,再插入节点 9,又不满足了:
这个时候,情况有点微妙,不像我们之前旋转的时候时候处理情况都比较简单,单纯左转满足不了需求,需要先将以 10 作为根节点的子树做一次右转,再将以 7 为根节点的子树做一次左转,让这棵不平衡子树转化为平衡子树:
这样整棵二叉树就满足平衡二叉树的要求了:
最后,我们插入节点 8,此时情况和刚才类似,这个时候,我们以 9 为根节点对子树进行右旋,再以 6 为根节点对子树进行左旋,最终达到平衡状态:
总结一下,大体的思路是平衡因子 BF 的值大于 1 时,右旋,小于 -1 时左旋,如果最小不平衡子树的 BF 值和其子树的 BF 值符号相反时,需要先将子树进行旋转使两者 BF 值符号相同,再旋转最小不平衡子树。我们将单纯的左旋、右旋叫做单旋处理,将需要两次旋转处理的操作叫做双旋处理。
平衡二叉树(AVL)的实现代码和算法复杂度
节点类
我们还是使用二叉链表来实现二叉树的存储,对应的节点类如下:
class AVLNode
{
public $data; // 节点数据
public $left = null; // 左子结点
public $right = null; // 右子节点
public $bf = 0; // 平衡因子BF
public $parent = null; // 存储父节点
public function __construct($data)
{
$this->data = $data;
}
}
和普通二叉树节点相比,新增了一个 $bf 属性用于存放平衡因子,以及一个 $parent 属性用于存放父级节点。
插入节点实现
平衡二叉树也是二叉排序树,所以查找实现和二叉排序树一样。下面我们来实现最关键的插入节点算法,我们创建了一个新的 AVLTree 类用来实现平衡二叉树的相关操作,编写新增节点相关方法如下:
class AVLTree
{
/**
* 根节点
* @var AVLNode
*/
private $root;
const LH = 1; // 左子树高(高度差)
const EH = 0; // 等高
const RH = -1; // 右子树高(高度差)
public function getTree()
{
return $this->root;
}
/**
* @param int $data
*/
public function insert(int $data)
{
$this->insert_node($data, $this->root);
}
/**
* 插入节点
* @param int $data
* @param AVLNode $tree
* @return bool
*/
protected function insert_node(int $data, &$tree)
{
if (!$tree) {
$tree = new AVLNode($data);
$tree->bf = self::EH;
return true;
}
if ($data < $tree->data) {
if (!$this->insert_node($data, $tree->left)) {
return false;
} else {
if (empty($tree->left->parent)) {
$tree->left->parent = $tree;
}
switch ($tree->bf) {
case self::LH:
$this->left_balance($tree);
return false;
case self::EH:
$tree->bf = self::LH;
return true;
case self::RH:
$tree->bf = self::EH;
return false;
}
}
} else {
if (!$this->insert_node($data, $tree->right)) {
return false;
} else {
if (empty($tree->right->parent)) {
$tree->right->parent = $tree;
}
switch ($tree->bf) {
case self::LH:
$tree->bf = self::EH;
return false;
case self::EH:
$tree->bf = self::RH;
return true;
case self::RH:
$this->right_balance($tree);
return false;
}
}
}
}
/**
* 右旋操作
* @param AVLNode $tree
*/
protected function right_rotate(&$tree)
{
$subTree = $tree->left; // 将子树的左节点作为新的子树根节点
if ($tree->parent) {
$subTree->parent = $tree->parent; // 更新新子树根节点的父节点
$left = false;
if ($tree->parent->left == $tree) {
$left = true;
}
} else {
$subTree->parent = null;
}
$tree->left = $subTree->right; // 将原来左节点的右子树挂到老的根节点的左子树
$tree->parent = $subTree;
$subTree->right = $tree; // 将老的根节点作为新的根节点的右子树
$tree = $subTree;
if (!$tree->parent) {
$this->root = $tree;
} else {
// 更新老的子树根节点父节点指针指向新的根节点
if ($left) {
$tree->parent->left = $tree;
} else {
$tree->parent->right = $tree;
}
}
}
/**
* 左旋操作
* @param AVLNode $tree
*/
protected function left_rotate(&$tree)
{
$subTree = $tree->right; // 逻辑和右旋正好相反
$oldTree = clone $tree;
if ($tree->parent) {
$subTree->parent = $tree->parent;
$left = true;
if ($tree->parent->right == $tree) {
$left = false;
}
} else {
$subTree->parent = null;
}
$tree->right = $subTree->left;
$tree->parent = $subTree;
$subTree->left = $tree;
$tree = $subTree;
if (!$tree->parent) {
$this->root = $tree;
} else {
if ($left) {
$tree->parent->left = $tree;
} else {
$tree->parent->right = $tree;
}
}
}
/**
* 左子树平衡旋转处理
* @param AVLNode $tree
*/
protected function left_balance(&$tree)
{
$subTree = $tree->left;
switch ($subTree->bf) {
case self::LH:
// 新插入节点在左子节点的左子树上要做右单旋处理
$tree->bf = $subTree->bf = self::EH;
$this->right_rotate($tree);
break;
case self::RH:
// 新插入节点在左子节点的右子树上要做双旋处理
$subTree_r = $subTree->right;
switch ($subTree_r->bf) {
case self::LH:
$tree->bf = self::RH;
$subTree->bf = self::EH;
break;
case self::EH:
$tree->bf = $subTree->bf = self::EH;
break;
case self::RH:
$tree->bf = self::EH;
$subTree->bf = self::LH;
break;
}
$subTree_r->bf = self::EH;
$this->left_rotate($subTree);
$this->right_rotate($tree);
}
}
/**
* 右子树平衡旋转处理
*/
protected function right_balance(&$tree)
{
$subTree = $tree->right;
switch ($subTree->bf) {
case self::RH:
// 新插入节点在右子节点的右子树上要做左单旋处理
$tree->bf = $subTree->bf = self::EH;
$this->left_rotate($tree);
break;
case self::LH:
// 新插入节点在右子节点的左子树上要做双旋处理
$subTree_l = $subTree->left;
switch ($subTree_l->bf) {
case self::RH:
$tree->bf = self::LH;
$subTree->bf = self::EH;
break;
case self::EH:
$tree->bf = $subTree->bf = self::EH;
break;
case self::LH:
$tree->bf = self::EH;
$subTree->bf = self::RH;
break;
}
$subTree_l->bf = self::EH;
$this->right_rotate($subTree);
$this->left_rotate($tree);
}
}
}
编写简单的测试代码
接下来,我们编写一段简单的测试代码来测试上述代码是否能够正常工作:
$avlTree = new AVLTree();
$avlTree->insert(3);
$avlTree->insert(2);
$avlTree->insert(1);
$avlTree->insert(4);
$avlTree->insert(5);
$avlTree->insert(6);
$avlTree->insert(7);
$avlTree->insert(10);
$avlTree->insert(9);
$avlTree->insert(8);
// 中序遍历生成的二叉树看是否是二叉排序树
midOrderTraverse($avlTree->getTree());
// 以数组形式打印构建的二叉树看是否是平衡二叉树
print_r($avlTree->getTree());
我们以上一篇分享的示例数据为例,通过上述插入节点代码将这些数据插入到二叉树中,看最终生成的二叉树是否是平衡二叉树。结果符合我们的预期,构建的二叉树和下面这个二叉树一模一样:
说明我们成功构建出了平衡二叉树。
平衡二叉树的节点删除也要不断去判断删除节点后是否还满足平衡二叉树的要求。
算法复杂度
我们在讲二叉排序树的插入、删除、查找时提到,最理想的情况下,时间复杂度是 O(logn),而平衡二叉树就是这种理想情况,虽然平衡二叉树性能是最好的,也是最稳定的,但是这套算法实现起来比较复杂,每次插入节点和删除节点都需要判断剩下节点构成的二叉排序树是否满足平衡二叉树的要求,如果不满足需要做相应的左旋右旋处理,维护成本高,因此,在工程实践上,我们更多时候使用的是红黑树这种二叉排序树,它是一种不严格的平衡二叉树,实现起来更加简单,性能也接近严格的平衡二叉树。