前面记录了二叉查找树,它在搜索方面的效率显而易见,可它也存在某种缺陷,假设我们连续插入较小或较大的数据,那么二叉查找树将会逐渐退变为一个线性结构,从而搜索就变为了线性查找,效率将会大打折扣。所以,我们需要一棵这样的树,它在插入新节点后,能够重新调整自己的结构,使左右恢复平衡。AVL树就符合这个条件。
AVL树是最先发明的自平衡二叉查找树,其得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 “An algorithm for the organization of information” 中发表了它。
在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树,其查找、插入和删除在平均和最坏情况下都是O(logN)。
首先是创建AVL树:
我们可以从一个排序后的数组来初始化AVL树,这个过程不难,只需找到数组区间的中间元素,这个将作为树的根节点,同时这个节点将数组划分两个子区间,然后分别再取左右子区间中间元素,创建左右子树的根节点,同时继续划分子区间,如此进行下去。过程如下图所示:
然后是插入和移除节点,基本操作跟二叉查找树相似,不同的是这两个操作都需要重新平衡子树结构。
要使一棵树重新恢复平衡,我们需要对子树节点进行旋转操作,而旋转操作需要应对4种不同的情况,它们分别是:
LL: 即左左情况,此时需要对节点进行右旋转操作
RR: 即右右情况,此时需要对节点进行左旋转操作
以上这两种情况的旋转操作如下图所示:
还有两种相对复杂的情况:
LR: 即左右情况,此时需要先进行一次RR型旋转,再进行一次LL型旋转
RL: 即右做情况,此时需要先进行一次LL行旋转,再进行一次RR型旋转
下面两张图分别拆解了这两种情况的步骤:
下面分别是JS和C语言的实现代码:
JS描述:
//AVL树节点结构
function AVLTreeNode(data) {
this.data = data;
this.leftChild = null;
this.rightChild = null;
this.height = 0;
}
var AVLTreeUtil = {
//由排序数组创建AVL树
createAVLTree: function(array, low, high) {
if (high < low) return null;
var mid = Math.floor((low + high) / 2);
var newNode = new AVLTreeNode(array[mid]);
newNode.leftChild = this.createAVLTree(array, low, mid - 1);
newNode.rightChild = this.createAVLTree(array, mid + 1, high);
this.updateHeight(newNode);
return newNode;
},
//前序遍历子树
preOrderTraverse: function(node, visitFn) {
if (node) {
visitFn(node);
this.preOrderTraverse(node.leftChild, visitFn);
this.preOrderTraverse(node.rightChild, visitFn);
}
},
//中序遍历子树
inOrderTraverse: function(node, visitFn) {
if (node) {
this.inOrderTraverse(node.leftChild, visitFn);
visitFn(node);
this.inOrderTraverse(node.rightChild, visitFn);
}
},
//插入节点并保持树平衡
insertNodeWithBalance: function(node, key) {
if (!node) {
return new AVLTreeNode(key);
}
if (key < node.data) {
node.leftChild = this.insertNodeWithBalance(node.leftChild, key);
//平衡当前节点和左子树
node = this.balanceLeft(node, key);
} else {
node.rightChild = this.insertNodeWithBalance(node.rightChild, key);
//平衡当前节点和右子树
node = this.balanceRight(node, key);
}
this.updateHeight(node);
return node;
},
//移除子树中指定值的节点
removeNodeWithBalance: function(node, key) {
if (!node) return null;
if (key < node.data) {
node.leftChild = this.removeNodeWithBalance(node.leftChild, key);
//平衡当前节点和左子树