要想了解AVL树,就得先知道二插搜树的性质:
如上图就是一棵二插搜索树
二插搜索树的查找效率正常情况下是 l o g 2 n log_{2}n log2n,但是在极端情况下如果这颗树转变成了单分支,也就是变成了链表形式,查找效率就是 O ( n ) O(n) O(n)了,这个时候AVL树的优势就来了。
AVL树又叫平衡二插搜索树,二插搜索树的查找效率在极端情况下是比较低的,而AVL树会保证左右子树的高度差的绝对值不会超过1,每次在插入新的节点后都会进行对应的调整,保证树的平衡。
一棵AVL数或者是空树,或者是会具有以下性质:
如果一棵 n n n个节点的二插搜索树的高度是平衡的,那么这个搜索树就是AVL树,那么可以它的高度就可以保持为 l o g 2 n log_{2}n log2n,查找的时间复杂度为 l o g 2 n log_{2}n log2n
AVL树的每一个节点的定义方式如下:
static class TreeNode {
// 值
int val;
// 左子树高度-右子树高度
int bf;// 平衡因子
TreeNode left; //左孩子
TreeNode right; // 右孩子
TreeNode parent;// 父节点引用
public TreeNode(int val) {
this.val = val;
}
}
AVL树遵循了搜索树的性质,按照搜索树的插入方式进行 插入就行
插入有几个逻辑:
接着就是进行平衡因子的调整
修改平衡因子后就需要进行判断,有三种情况:
上图是正常情况下的插入,插入元素后整棵树还是平衡的。但如果是其它情况就要进行旋转了:
/**
* AVL树插入元素
* @param val
*/
public boolean insert(int val) {
TreeNode newNode = new TreeNode(val);
if (root == null) {
// 第一次插入
root = newNode;
return true;
}
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
parent = cur;
if (cur.val > val) {
cur = parent.left;
} else if (cur.val == val) {
System.out.println("插入失败元素已经存在");
return false;
} else {
cur = parent.right;
}
}
// 在对应位置插入新元素
if (parent.val > val) {
parent.left = newNode;
} else {
parent.right = newNode;
}
newNode.parent = parent;
cur = newNode;
// 调整平衡因子
while (parent != null) {
if (parent.left == cur) {
parent.bf--;
} else {
parent.bf++;
}
if (parent.bf == 0) {
// 说明所有树已经平衡,无需调整
break;
} else if (parent.bf == 1 || parent.bf == -1) {
// 当前子树是平衡的,但不能说明整棵树平衡,继续向上调整
cur = parent;
parent = cur.parent;
} else {
if (parent.bf == 2) {
if (cur.bf == 1) {
// 进行左旋
rotateLeft(parent);
} else {
// cur.bf == -1
// 右左双旋
rotateRL(parent);
}
}else {
//parent.bf == -2
if (cur.bf == -1) {
// 进行右旋
rotateRight(parent);
} else {
// cur.bf == 1
// 左右双旋
rotateLR(parent);
}
}
// 调整完后树已经平衡
break;
}
}
return true;
}
当新节点插入到较高左子树的左侧,此时就会出现平衡因子为-2,其子节点为-1,就需要进行右单旋。右单旋其实就是降低左树的高度来提升右树的高度
右单旋步骤:
/**
* 右单旋
* @param parent
*/
private void rotateRight(TreeNode parent) {
// 记录对应节点
TreeNode parentL = parent.left;
TreeNode parentLR = parentL.right;
TreeNode pParent = parent.parent;
// 如果parentLR存在
if (parentLR != null) {
parentLR.parent = parent;
}
parent.left = parentLR;
parent.parent = parentL;
parentL.right = parent;
// 要调整的是根节点
if (parent == root){
root = parentL;
parentL.parent = null;
} else {
// 如果不是根节点就需要判断,当前子树是parent的左子树还是右子树
if (pParent.left == parent) {
pParent.left = parentL;
} else {
pParent.right = parentL;
}
parentL.parent = pParent;
}
// 调整平衡因子
parentL.bf = 0;
parent.bf = 0;
}
当把新节点插入到AVL树中较高右子树的右侧后,调整平衡因子发现节点的平衡因子为2且它的子树为1,此时就需要进行左单旋了。左单旋其实就是降低右树的高度来提升左树的高度。
左单选步骤:
/**
* 左单旋
* @param parent
*/
private void rotateLeft(TreeNode parent) {
// 记录对应节点
TreeNode parentR = parent.right;
TreeNode parentRL = parentR.left;
TreeNode pParent = parent.parent;
// 修改节点
parent.right = parentRL;
// 如果parentRL存在
if (parentRL != null) {
parentRL.parent = parent;
}
parent.parent = parentR;
parentR.left = parent;
// 如果旋转的是根节点
if (parent == root) {
root = parentR;
parentR.parent = null;
} else {
// 如果旋转的不是根节点就判断旋转的是pParent的左子树还是右子树
if (pParent.left == parent) {
pParent.left = parentR;
} else {
pParent.right = parentR;
}
parentR.parent = pParent;
}
// 更新平衡因子
parent.bf = 0;
parentR.bf = 0;
}
有些情况下,单纯对树进行左旋或者右旋还是无法保证树是平衡状态,所以此时就需要双旋。比如在较高左子树的右侧插入一个新元素,就需要进行左右双旋。
插入时需要考虑两种情况,一个是插入到左节点和插入到右节点:根据不同情况下的修改负载因子是不一样的,要进行特判。通过parentLR的平衡因子来判断新元素插入左节点还是右节点
插入到较高左子树的右侧的左节点
插入到较高左子树的右侧的右节点
假设我们以元素插入到较高左子树的右侧的右节点为例子:
最后调整平衡因子有两种情况:
/**
* 进行左右双旋
* @param parent
*/
private void rotateLR(TreeNode parent) {
// 记录相关节点
TreeNode parentL = parent.left;
TreeNode parentLR = parentL.right;
int bf = parentLR.bf;
// 先左旋parent.left
rotateLeft(parentL);
// 再右旋parent
rotateRight(parent);
// 修改平衡因子
// 分两种情况
if (bf == -1) {
// 插入到较高左子树右侧的左子树
parent.bf = 1;
parentL.bf = 0;
parentLR.bf = 0;
} else if (bf == 1) {
//bf == 1
// 插入到较高左子树右侧的右子树
parentL.bf = -1;
parentLR.bf = 0;
parent.bf = 0;
}
}
右左双旋是当元素插入在较高右子树的左侧发生的。插入后要考虑两种情况,一个是元素插入在较高右子树的左侧的左节点,另外一种是元素插入在较高右子树的左侧的右节点。
插入到较高右子树左侧的左节点
插入到较高右子树左侧的右节点
以插入到较高右子树左侧的右节点为例子
最后调整平衡因子有两种情况:
/**
* 进行右左双旋
* @param parent
*/
private void rotateRL(TreeNode parent) {
// 记录相关节点
TreeNode parentR = parent.right;
TreeNode parentRL = parentR.left;
int bf = parentRL.bf;
rotateRight(parentR);
rotateLeft(parent);
if (bf == -1) {
parentR.bf = 1;
parent.bf = 0;
parentRL.bf = 0;
} else if (bf == 1) {
parent.bf = -1;
parentR.bf = 0;
parentRL.bf = 0;
}
}
AVL树删除元素,先要找到该元素再进行删除,但这里需要考虑到多种情况。
针对左右不为空的情况采用替换删除:
验证AVL树采用判断每一个子树的左右子树高度差作为判断(右子树高度 − - −左子树高度),同时验证父节点的平衡因子的是否对应该差值。
才用后序遍历进行减枝,从AVL树的叶子节点从底之顶进行判断,可以避免重复判断,只要有一棵子树不平衡就无需判断其它节点了。
时间复杂度 O ( n ) O(n) O(n)
空间复杂度 O ( n ) O(n) O(n)
/**
* 判断是否AVL树
* @return
*/
public boolean isBalanced() {
return balanced(root) >= 0;
}
public int balanced(TreeNode root) {
if (root == null) {
return 0;
}
int left = balanced(root.left);
int right = balanced(root.right);
if (right-left != root.bf) {
System.out.println("节点:"+root+" 平衡因子出现问题");
return -1;
}
// 当有一颗子树不平衡时就无需判断其它节点了
if (left >= 0 && right >= 0 && Math.abs(right-left) < 2) {
return Math.max(right,left)+1;
} else {
return -1;
}
}
AVL是一棵高度绝对平衡的二插搜索树,该树要求每个节点的左右子树高度差的绝对值不能超过1,这样可以保证其查询的时间复杂度为 O ( l o g 2 n ) O(log_{2}n) O(log2n),但如果频繁对AVL进行删除和插入操作,性能是非常低的。插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置 。所以如果需要一种查询速度快且数据有序的数据结构,并且只对这些数据进行查询就可以使用AVL树,一旦设计到插入和删除就不适合使用AVL树了。