数据结构树之平衡二叉树
往期文章传送门:
上一篇我们已经介绍了树以及树的相关特性,也实现了一个二叉搜索树(BST)也了解BST的遍历、搜索、增加和移除节点的操作。通过前面的学习聪明的你一定发现二叉搜索树在增加元素的时候存在一个问题,当一直添加大于根节点的元素时候,树就会向一侧倾斜。如下图所示:
这样在倾斜的边上添加、移除和搜索某个节点时引起一些性能问题。
为了解决这个问题,有一种树叫作Adelson-Velskii-Landi树(AVL树)。
AVL树是一种自平衡二叉搜索树,意思是任何一个节点左右两侧子树的高度之差最多为1。
平衡二叉树(AVL)
平衡二叉树添加和删除元素后,会进行结构的调整,让任意一个节点的左子树和右子树高度之差绝对值小于等于1;
那么如何创建一个平衡二叉树的类呢?
平衡二叉树也是二叉搜索树的一种,他继承于二叉搜索树,实现AVL树类代码如下:
import BinarySearchTree from './BinarySearchTree'
import {defaultCompare, COMPARE, BALANCEFACTOR} from "../util";
import {TreeNode} from "./TreeNode";
export default class AVLTree extends BinarySearchTree{
constructor(compareFn = defaultCompare ) {
super(compareFn);
this.compareFn = compareFn;
this.root = null;
this.count = null;
}
}
AVL树继承于BST树所以AVL具有BST中:插入、删除、搜索、遍历这些方法,对于AVL树来说只有在插入和删除的时候会进行树的平衡调整,其他api方法是完全遵循BST树的,接下来我们来实现AVL树的insert、remove方法,不过在此之前我们先来了解节点的高度和平衡因子;
节点的高度和平衡因子
所谓节点的高度是指节点到任意子节点边的最大值,也可以说是节点的深度,如下图所示:
从图中我们可以看出各个节点的高度(红色数字),所谓的高度就是节点左右边数最大的值;
怎么计节点的高度呢?
实现如下:
getNodeHeight(node) {
if (!node) {
return -1;
}
return Math.max(this.getNodeHeight(node.left), this.getNodeHeight(node.right)) + 1;
}
在AVL平衡树中,任何节点右子树的高度(hr)与左子树的高度(hl)之差的绝对值都要小于等于1,也就是值:-1、0、1三个值,如果不满足则需要平衡这棵树;我们把这个差值(-1、0、1)称作平衡因子,可以根据平衡因子来对树进行旋转调整,使树达到平衡状态。
平衡因子的计算方式如下所示:
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;
}
}
平衡AVL的旋转
当向平衡二叉树中插入或者删除节点后,就会要对节点进行验证,看是否需要进行再平衡,在平衡中会进行旋转,旋转包括:左-左:向右的单旋转;右-右:向左的单旋转;左-右:向右的双旋转(先LL旋转,再RR旋转);右-左:向左的双旋转(先RR旋转,再LL旋转);
1、左-左(LL):向右边单旋转
这种情况出现于节点的左侧子节点的高度大于右侧子节点的高度时,并且左侧子节点也是平衡或左侧较重的,如下图所示。
看下图一个实例:
从图中可知刚开始树是平衡的,后面向树中插入节点12后,整体树失去了平衡向左倾斜或者说左侧较重,通过平衡因子的计算,会发现根节点22的平衡因子(左子树高度 - 右子树高度)为2,不属于-1,、0、1三个值,所以要进行树的再平衡调整。
调整过程如下步骤:
1、和平衡调整相关的三个节点分别是:
22、17、15,将节点17置于节点22(平衡因子为+2)所在的位置;
2、节点17的左节点保持不变,并将节点22的左节点置为节点17的右节点(20);
3、将节点17的右节点置为节点22;
经过LL调整就得到了右侧的结构了,实现代码如下:
rotationLL(node) {
const tempNode = node.left;
node.left = tempNode.right;
tempNode.right = node;
return tempNode;
}
2、右-右(RR):向左边单旋转
这种情况和第一种LL刚刚相反,它出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点也是平衡或右侧较重的,如下图所示:
看下图一个实例:
从图中我们可以知道在插入节点11后,导致整棵树右边倾斜失去平衡;
根节点的平衡因子为-2,绝对值是大于1的,所以要进行整体树的调整,而树满足RR单向旋转,旋转后重新达到平衡如图右侧;
具体的旋转步骤如下:
1、旋转相关节点分别为:
3、6、8,将节点6置于节点3(平衡因子是-2)的位置;
2、将节点6的左节点置于3的右节点,节点6的右节点保持不变;
3、然后将节点3置于节点6的左节点;
经过三步就完成了节点旋转;
让树重新保持平衡。
代码实现如下:
rotationRR(node) {
const tempNode = node.right;
node.right = tempNode.left;
tempNode.left = node;
return tempNode;
}
3、左-右(LR):向右的双旋转
这种情况出现于左侧子节点的高度大于右侧子节点的高度,并且左侧子节点右侧较重。在处理这种情况,我们可以对左侧子节点进行左旋转(RR)来修复,这样会形成左-左的情况,然后再对不平衡的节点进行一个右旋转(LL)来修复,如下图所示。
可以看如下图实例:
从上图可以看出来向平衡树中插入节点23,使整棵树失去平衡,并且符合LR类型;
所以按照步骤进行旋转:
1、先对节点24的左侧节点进行左单旋转(RR),此刻和旋转相关的元素分别是:
15、21、23;
2、将节点21置于节点15,并将节点21的左节点置于节点15的右节点;
3、将节点15置于节点21的左节点,而节点21的右节点保持不变,这就完成了对左子节点进行RR旋转;
4、上面3步得到的树是一颗LL树,此时要进行LL旋转;
5、进行LL旋转和旋转相关的节点:
24、21、15,将节点21置于节点24的位置;
6、将节点21的右节点置于节点24的左节点,同时将节点24置于节点21的右节点;
7、节点21的左节点和节点24的右节点保持不变,就完成了LR旋转,实现树的再平衡;
代码实现如下:
rotationLR(node) {
node.left = this.rotationRR(node.left); // 先将失去平衡节点的左子树进行左旋转;
return this.rotationLL(node); // 再对整个失去平衡的树进行右单旋转
}
4、右-左(R-L):向左的双旋转
这种情况出现于右侧子节点的高度大于左侧子节点的高度,并且右侧子节点左侧较重。在处理这种情况,我们可以对右侧子节点进行右旋转(LL)来修复,这样会形成右-右的情况,然后再对不平衡的节点进行一个左旋转(RR)来修复,如下图所示。
看如下实例:
实现过程和上面的L-R相对的,这里面就不再赘述了,代码实现如下:
rotationRL(node) {
node.right = this.rotationLL(node.right); // 先将失去平衡节点的右子树进行右旋转
return this.rotationRR(node); // 再对整个失去平衡的树进行左旋转
}
了解了这四种旋转方式后我们来实现平衡树的插入和删除节点
节点的插入:insert(key)
插入一个节点元素会经历如下几个过程:
1、首先会和BST规则一样将节点插入树中;
2、然后会计算节点的平衡因子;
3、根据平衡因子进行相对应的旋转;
4、经过调整后树达到平衡;
代码实现如下:
insert(key) {
this.root = this.insertNode(this.root, key);
}
insertNode(node, key) {
debugger
if (!node) { // 说明已经到了叶节点没有下一个节点了,这个时候可以创建一个新节点作为上一个节点的子节点
return new TreeNode(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); // 获取该节点的平衡因子
if (balanceFactor === BALANCEFACTOR.UNBALANCED_LEFT) { // 说明是左子树失去平衡
if(this.compareFn(key, node.left.key) === COMPARE.LESS_THAN) { // 如果新插入的数比失去平衡的左子树数要小,则是LL型旋转
node = this.rotationLL(node);
} else { // 否则说明新增元素是插入失去平衡节点的左子树的右侧插入,则是LR旋转
return this.rotationLR(node);
}
}
if (balanceFactor === BALANCEFACTOR.UNBALANCED_RIGHT) { // 说明是右子树失去平衡
if (this.compareFn(key, node.right.key) === COMPARE.BIGGER_THAN) { // 如果新插入的数比失去平衡的右子树数要大,则是RR型旋转
node = this.rotationRR(node);
} else { // 否则说明新增元素是插入失去平衡节点的右子树的左侧插入,则是RL旋转
return this.rotationRL(node);
}
}
return node;
}
测试代码:
avltree.insert(7);
avltree.insert(8);
avltree.insert(9);
avltree.insert(10);
avltree.insert(11);
avltree.insert(12);
avltree.insert(13);
avltree.insert(18);
avltree.insert(25);
let avlarr = [];
avltree.inOrderTraverse((key) => avlarr.push(key));
console.log(avlarr, '中序:inOrderTraverse');//[7, 8, 9, 10, 11, 12, 13, 18, 25]
节点的删除remove(key)
自平衡二叉树在删除某个节点的时候和BST(二叉搜索树)一样先找到这个节点进行删除,删除的情况也会分为删除叶节点、有左子节点或者右子节点、既有左子节点又有右子节点;删除完节点后,会进行平衡因子的计算,再根据平衡因子做相应的平衡调整,使树达到平衡;代码实现如下:
remove(key){
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
node = super.removeNode(node, key);
if (!node) {
return node; // 不需要调整平衡;
}
const balanceFactor = this.getBalanceFactor(node);
if(balanceFactor === BALANCEFACTOR.UNBALANCED_LEFT) {
const balanceFactorLeft = this.getBalanceFactor(node.left);
if(balanceFactorLeft === BALANCEFACTOR.BALANCED || balanceFactorLeft === BALANCEFACTOR.SLIGHTLY_UNBALANCED_LEFT) {
return this.rotationLL(node);
}
if (balanceFactorLeft === BALANCEFACTOR.SLIGHTLY_UNBALANCED_RIGHT) {
return this.rotationLR(node.left);
}
}
if(balanceFactor === BALANCEFACTOR.UNBALANCED_RIGHT) {
const balanceFactorRight = this.getBalanceFactor(node.right);
if(balanceFactorRight === BALANCEFACTOR.BALANCED || balanceFactorRight === BALANCEFACTOR.SLIGHTLY_UNBALANCED_RIGHT) {
return this.rotationRR(node);
}
if(balanceFactorRight === BALANCEFACTOR.SLIGHTLY_UNBALANCED_LEFT) {
return this.rotationRL(node.right);
}
}
return node;
}
小结
本文主要介绍了一下平衡二叉树的实现原理,关键点要理解几个关键概念:
1、树的高度;
2、平衡因子计算;
3、4种旋转规则(LL\LR\RR\RL);
4、以及实现平衡二叉树添加和删除元素;
其中4种旋转规则要重点理解,什么情况进行LL旋转什么时候是满足LR旋转这些,反复看几遍就能明白其中的道理了,下一次将为你讲解数据结构图,敬请期待;