Why need AVL tree?
当我们在使用数据结构的时候,最为重要也被人们使用的最多的两种操作是:查找(Search)和排序(Sort)。针对排序,我们已经接触过很多排序算法(快排、归并、插排......)来优化排序的时间效率。而针对查找操作,我们也当然需要一个算法或者数据结构能够进行高效且稳定的进行排查找,毕竟一个网上公开的数据库每天可能会面对数千万次的访问查找操作,总不可能总是以暴力穷举的方法查找吧!那得是多少O(n)呐!
所以我们之前引出了二分查找法,继而又提出了二叉搜索树(same as 二叉查找树)。二叉搜索树(Binary Search Tree,BST)是一种数据结构,它是一种特殊的二叉树,每个节点都包含一个键值和相关的数据。在BST中,对于任意一个节点,其左子树中的所有节点的键值都小于该节点的键值,右子树中的所有节点的键值都大于该节点的键值。这种特性使得在BST中查找、插入和删除操作可以采用高效的递归或迭代算法,使得BST在进行这些操作的时候能够拥有O(log n)的时间复杂度。
然而,BST的性能是不够稳定的,它对输入数据的顺序敏感,当输入数据的顺序是特定的,例如连续的整数或近乎有序的数据,BST可能会失去平衡。如上图右半部分,如果插入的元素顺序恰好是有序的(例如,连续的整数),BST可能会出现瘦树,也就是退化为一个链表,使得搜索、插入和删除的时间复杂度变为O(n),其中n是树中的节点数。这是因为在这种情况下,BST的高度会接近n,相对比理想的平衡树的高度应该是log n。
此时,为了解决原生BST的不稳定性,由苏联计算机科学家格尔德·阿德尔松-维尔斯ky和埃米尔·兰尼斯于1962年发明,AVL树作为一种自平衡二叉查找树粉墨登场。
What is AVL tree
AVL树就是对二叉搜索树的一种改进,它是一种自平衡二叉搜索树。AVL树引入了平衡因子的概念,即一个节点的左子树高度减去右子树高度的差。在AVL树中,要求任何节点的平衡因子的绝对值不超过1,这意味着树的左右两个子树的高度最多相差1,从而保证了树的平衡。
通过强制保持这种平衡,AVL树能够在插入和删除操作后进行必要的旋转操作(如左旋、右旋、左右旋和右左旋),以重新调整树的结构,确保其满足平衡条件。这样,即使在最坏的输入情况下,AVL树也能保持其高度为O(log n),从而确保搜索、插入和删除等操作的时间复杂度始终保持在O(log n)。
AVL树改进了二叉搜索树的主要原因是通过引入自平衡机制,解决了二叉搜索树在特定数据插入顺序下可能退化为线性时间复杂度的问题,确保了在各种数据分布情况下都能提供稳定且高效的搜索性能。
定义
在AVL树中,每个节点都包含一个键值(key)和相关的数据(value)(value可加可不加,我的演示demo没加),以及指向其左右子节点的指针。此外,每个节点还具有一个平衡因子(balance factor),它是该节点的左子树高度减去右子树高度的差。
图解“高度(height)”和“平衡因子(balance factor)”
结点定义
AVL树的结点和普通二叉树的结点差别不大,对于AVL结点我们可以选择在结点类中直接定义节点高度和平衡因子,也可以通过定义成员方法来在算法中调用计算。
这里我选择三叉链表的方式来定义结点:
left | data | parent | right |
public class TreeNode<T> {
T data;
int height;
TreeNode<T> parent;
TreeNode<T> left;
TreeNode<T> right;
boolean isThreadedLeft, isThreadedRight;
public TreeNode() {
right = left = null;
}
public TreeNode(T data) {
this.data = data;
right = left = null;
this.isThreadedLeft = this.isThreadedRight = false;
}
public TreeNode(T data, TreeNode<T> left, TreeNode<T> right) {
this.data = data;
this.left = left;
this.right = right;
this.isThreadedLeft = this.isThreadedRight = false;
}
public int getHeight(TreeNode<T> node) {
return node == null ? 0 : 1 + Math.max(getHeight(node.left),getHeight(node.right));
}
/**
* @return true when height are changed after update;
* false if height not change
*/
public boolean updateHeight(TreeNode<T> node) {
int newHeight = getHeight(node);
if (node.height != newHeight) {
node.height = newHeight;
return true;
}
return false;
}
// Define left > right to be positive balance
public int getBalance(TreeNode<T> node) {
return node == null ? 0 : getHeight(node.left) - getHeight(node.right);
}
}
注意在AVL树中的height是经常会发生变化的,它是一个动态量,所以我们需要定义一些方法来实现对结点check、update和modify。
在结点类中,我们定义与结点height相关的成员方法,以便在AVL树中对结点高度的获取与操作:
成员变量:
height:用来保存某个时刻下结点的高度,方便to check。
成员方法:
getHeight():不对外提供(private修饰),递归确定结点在树中所处在的真实高度。
updateHeight():对外提供(public修饰),当结点高度可能发生改变时我们使用该方法对height更新。
getBalance():对外提供(public修饰),获取此结点的平衡情况,定义为左边>右边时为正平衡。
AVL tree的实现
类的基本实现
定义AVL树的成员变量和构造器:
//Self-balancing binary search tree
public class AVLTree<T extends Comparable> extends BSTree<T>{
private TreeNode<T> root;
public AVLTree() {
this.root = null;
}
public AVLTree(TreeNode<T> root) {
this.root = root;
}
AVL树的旋转
当我们修改AVl树中结点时,树的局部可能会出现四种不同的失衡状态:LL型、RR型、LR型、RL型。
我们通过插入结点的情况来理解:
前两种失衡状态为基本情况,当我们理解了前两种状态的原理和代码,后两种状态也就是前两种的延申,非常好理解。
RR型(左旋)
往一个树右子树的右子树上插入一个节点,导致二叉树变得不在平衡,如下图,往平衡二叉树中插入5,导致这个树变得不再平衡,此时需要左旋操作,如下:
代码实现:
// RR condition--need left rotation
private TreeNode<T> leftRotate(TreeNode<T> oldRoot) {
TreeNode<T> newRoot = oldRoot.right; // pick oldRoot.rightSub as new root
// oldRoot < newRoot.left < newRoot
TreeNode<T> newRight = newRoot.left; // pick new root leftSub to fit oldRoot.rightHole
TreeNode<T> parent = oldRoot.parent;
// perform left rotate
//1.newRoot replace oldRoot pos
if (parent != null) {
if (oldRoot.data.compareTo(parent.data) < 0) parent.left = newRoot;
else parent.right = newRoot;
}
// 2.reassemble oldRoot(newRoot.left link to oldRoot right)
oldRoot.right = newRight;
if (newRoot.left != null) {
newRight.parent = oldRoot;
}
// 3.oldRoot rotate to newRoot left
newRoot.left = oldRoot;
oldRoot.parent = newRoot;
// 4.after rotate Height of new/old Root changed i.e. update height
oldRoot.updateHeight(oldRoot);
newRoot.updateHeight(newRoot);
return newRoot;
}
LL型(右旋)
往一个AVL树左子树的左子树上插入一个节点,导致二叉树变得不在平衡,如下图,往平衡二叉树中插入2,导致这个树变得不再平衡,此时需要右旋操作,如下:
代码实现:
// LL condition--need right rotation
private TreeNode<T> rightRotate(TreeNode<T> oldRoot) {
TreeNode<T> newRoot = oldRoot.left; // pick oldRoot.rightSub as new root
// newRoot < newRoot.right < oldRoot
TreeNode<T> newLeft = newRoot.right; // pick new root leftSub to fit oldRoot.leftHole
TreeNode<T> parent = oldRoot.parent;
// perform right rotate
//1.newRoot replace oldRoot pos
if (null != parent ) {
if (oldRoot.data.compareTo(parent.data) < 0) {
parent.left = newRoot;
}else {
parent.right = newRoot;
}
}
// 2.reassemble oldRoot(newRoot.right link to oldRoot left)
oldRoot.left = newLeft;
if (newRoot.right != null) {
newLeft.parent = oldRoot;
}
// 3.oldRoot rotate to newRoot right
newRoot.right = oldRoot;
oldRoot.parent = newRoot;
// 4.after rotate Height of new/old Root changed i.e. update height
oldRoot.updateHeight(oldRoot);
newRoot.updateHeight(newRoot);
return newRoot;
}
后两种失衡状态在原理上会稍显复杂,当我们进行基本旋转时,左旋总是需要把右子树的左子树接到新根左子树(也就是旧根)的右子树,右旋总是需要把左子树的右子树接接到新根右子树(也就是旧根)的左子树。
而当我们新插入的结点处在失衡根的左子树的右子树(LR型)或右子树的左子树(RL型)时,就会导致一些问题,如图所示:
会发现单步的基本旋转把新增结点的子树给接过旧根去了,这样就很bad,LR右旋变成LR,LR左旋变成RL,转完了树还是失衡,根本没变化嘛!
于是我们通过对发生变化的子树进行预旋转将问题从LR/RL型convert to LL/RR型来化解问题。这里引出左右旋和右左旋:
LR(先左旋再右旋)
往AVL树左子树的右子树
上插入一个节点,导致该树不再平衡,需要先对左子树进行左旋
,再对整棵树右旋
,如下图所示,插入节点为5.
代码很简单:
// LR condition--need left-right rotation
private TreeNode<T> leftRightRotate(TreeNode<T> node) {
// Check if the left child's right child is null
if (node.left.right == null) {
return rightRotate(node);
}
//1st leftRotate left subTree
node.left = leftRotate(node.left);
//2nd rightRotate root
return rightRotate(node);
}
RL(先右旋再左旋)
往AVL树右子树的左子树
上插入一个节点,导致该树不再平衡,需要先对右子树进行右旋
,再对整棵树左旋
,如下图所示,插入节点为2.
代码so easy:
// RL condition--need right-left rotation
private TreeNode<T> rightLeftRotate(TreeNode<T> node) {
// Check if the right child's left child is null
if (node.right.left == null) {
return leftRotate(node);
}
//1st leftRotate left subTree
node.right = rightRotate(node.right);
//2nd rightRotate root
return leftRotate(node);
}
AVL树基本数据操作
adjustBalance(平衡判断与调整)
/**
* check the balance condition of node and modify tree if needed
* Mind that no need to update height as already update in rotation
*/
private TreeNode<T> adjustBalance(TreeNode<T> node) {
if(node==null) return null;
//check balance
int bf = node.getBalance(node);
//Rotation
//left>right -> LL/LR type - right Rotate
if (bf >= 2) {
//LR type - 1st leftRotate->2nd rightRotate
if (node.left.getBalance(node.left) <= -1 ) {
System.out.println("leftRightRotate");
node = leftRightRotate(node);
}else {
System.out.println("rightRotate");
node = rightRotate(node);
}
}
//right>left -> RR/RL type - left Rotate
else if (bf <= -2) {
//RL type - 1st rightRotate->2nd leftRotate
if (node.right.getBalance(node.right) >= 1) {
System.out.println("rightLeftRotate");
node = rightLeftRotate(node);
}else {
System.out.println("leftRotate");
node = leftRotate(node);
}
}
return node;
}
insert(增加)
insert首先存在一个外部入口,其中包含checker部分以确保代码鲁棒性;
insert主体分两个部分,part1与正常BST的添加操作无异;part2进行AVL树的平衡检查和调整。
public void insert(T data) {
// checker
if (this.root == null) {// tree empty
this.root = new TreeNode<>(data);
return;
} else if (data == null) {//data is illegal
System.out.println("data illegal");
return;
}
if (this.root.data != null) {
//!!!remember to fresh this.root
//root after insert are probably after rotate,
//thus newRoot have to assign back to this.root
this.root = insert(this.root, data);
}
}
public TreeNode<T> insert(TreeNode<T> root, T data) {
// add to left
if (data.compareTo(root.data) < 0) {
if (root.left == null) {
root.left = new TreeNode<>(data);
}else {
insert(root.left, data);
}
}//add to right
else if (data.compareTo(root.data) > 0) {
if (root.right == null) {
root.right = new TreeNode<>(data);
}else {
insert(root.right, data);
}
}//elem exist already
else {
System.out.println("element already exist");
}
//update height
root.updateHeight(root);
// adjust Balance
root = adjustBalance(root);
return root;
}
delete(删除)
绝赞更新中!!!
测试
public static void main(String[] args) {
AVLTree<Integer> avlT = new AVLTree<>();
// Test Left-Left (LL) rotation
System.out.println("LL Rotation:");
avlT.insert(50);
avlT.insert(30);
avlT.insert(20);
avlT.inOrder(avlT.root);
System.out.println();
// Test Right-Right (RR) rotation
avlT = new AVLTree<>();
System.out.println("RR Rotation:");
avlT.insert(20);
avlT.insert(30);
avlT.insert(40);
avlT.inOrder(avlT.root);
System.out.println();
// Test Left-Right (LR) rotation
avlT = new AVLTree<>();
System.out.println("LR Rotation:");
avlT.insert(50);
avlT.insert(30);
avlT.insert(40);
avlT.insert(20);
avlT.inOrder(avlT.root);
System.out.println();
// Test Right-Left (RL) rotation
avlT = new AVLTree<>();
System.out.println("RL Rotation:");
avlT.insert(20);
avlT.insert(40);
avlT.insert(30);
avlT.inOrder(avlT.root);
System.out.println();
}