一.AVL树是什么
在前面的学习中我们已经学习了二叉搜素树,二叉搜素树主要用于查询。二叉搜素树的查询效率为o(n),当树有序的时候二叉搜素树就变为一颗单分支的树,树的高度为n,所以最坏情况下时间复杂度为o(n)。
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
AVL树的特点:1.他的左右子树都是AVL树
2.左右子树高度之差不超过1(右子树减去左子树高度小于1)
二.AVL树节点的定义
class AVLTree {
static class TreeNode {
public int val;
public int bf;//平衡因子
public TreeNode left;//左孩子的引用
public TreeNode right;//右子树的引用
public TreeNode parent;//父亲节点的引用
public TreeNode(int val) {
this.val = val;
}
}
我们需要把AVL定义为三叉树,同时增加bf记录每个节点的平衡因子,通过获取节点的平衡因子来调节树的平衡。三叉结构增加一个指针指向父亲节点,可以更好的在增加节点时修改父亲节点的平衡因子。
三.AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:1. 按照二叉搜索树的方式插入新节点。2. 调整节点的平衡因子
public boolean insert(int val) {
TreeNode node = new TreeNode(val);
if(root == null) {
root = node;
return true;
}
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
if(cur.val < val) {
parent = cur;
cur = cur.right;
}else if(cur.val == val) {
return false;
}else {
parent = cur;
cur = cur.left;
}
}
//cur == null
if(parent.val < val) {
parent.right = node;
}else {
parent.left = node;
}
//
node.parent = parent;
cur = node;
上述代码为二叉搜素树的插入,前面已经学习过,但注意相比于二叉搜素树AVL树多了每个节点的parent域必须指向父亲节点。
如果在插入节点时,插入的节点比父亲节点大,那么该节点在父亲节点的右分支,bf++,反之如比父亲节点小,应当在父节点的左分支,所以bf–。
while (parent != null) {
//先看cur是parent的左还是右 决定平衡因子是++还是--
if(cur == parent.right) {
//如果是右树,那么右树高度增加 平衡因子++
parent.bf++;
}else {
//如果是左树,那么左树高度增加 平衡因子--
parent.bf--;
}
我们每插入一个节点都需要更新父亲节点的平衡因子,总共可分为三种情况:
1.如果更新后父亲节点的平衡因子为0,说明没有插之前父亲节点的平衡因子为1或者-1。在插入该节点后变为0,又说明正好插入矮的一边父亲节点因此平衡,此时树的高度不变,不用继续向上调整。
2.如果更新后平衡因子为1或者-1,说明未插入之前父亲节点的高度为0,插入后父亲节点变得不平衡,此时需要向上调整更新祖先节点的高度,最多会更新到根节点。
3.如果向上调整的过程中某个节点的平衡因子变为2或者-2此时该树不平衡,不是AVL树,我们需要调整这颗树使其成为AVL树,要将其进行旋转。
平衡因子为0时不需要调整直接返回:
平衡因子为1或者-1时需要向上调整
平衡因子为2或者-2时需要旋转:
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;
四.AVL树的旋转
1.右旋
由于60节点的平衡因子为-2所以不满足AVL树的定义,需要对该节点进行旋转。左树比右树高,所以需要减少左树的高度增加右树的高度,简单的来说就是把30节点提上去,60节点压下来。为了完成该操作首先让60节点的左树链接40节点,然后30节点右边链接60节点把60节点压下来,在更改30节点和60节点的parent指针的指向。旋转后整棵树平衡,旋转前30节点和60节点的bf分别为-1和-2,旋转后树平衡了,所以还需要改30节点和60节点的bf为0。
代码实现:
private void rotateRight(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
parent.left = subLR;
subL.right = parent;
if(subLR != null) {
subLR.parent = parent;
}
//必须先记录
TreeNode pParent = parent.parent;
parent.parent = subL;
//检查 当前是不是就是根节点
if(parent == root) {
root = subL;
root.parent = null;
}else {
//不是根节点,判断这棵子树是左子树还是右子树
if(pParent.left == parent) {
pParent.left = subL;
}else {
pParent.right = subL;
}
subL.parent = pParent;
}
subL.bf = 0;
parent.bf = 0;
}
但是也有可能是sublR为空的情况所以在写subLR.parent = parent;之前得先判空不然会空指针异常。该图如下:
在插入后还得考虑在插入前parent是否为根节点,如果为根节点需要把sl节点改为根节点,再把sl节点的parent指针改为空。如果不是根节点需要考虑parent节点在原来父亲节点的左边还是右边,如果是左边需要把调整后新的子树的根节点链接到上一个节点的左边,反之右边。但是调整后找不到该子树根节点原来的父亲节点,所以需要把它调整前保存起来。
该图为左边的情况:
2.AVL树的左旋
左单旋和右但旋类似所以直接看代码:
private void rotateLeft(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
parent.right = subRL;
subR.left = parent;
if(subRL != null) {
subRL.parent = parent;
}
TreeNode pParent = parent.parent;
parent.parent = subR;
if(root == parent) {
root = subR;
root.parent = null;
}else {
if(pParent.left == parent) {
pParent.left = subR;
}else {
pParent.right = subR;
}
subR.parent = pParent;
}
subR.bf = parent.bf = 0;
}
左旋也要注意判空和是不是根节点的问题。
3.左右双旋转
我们前面讨论的旋转都是一条直线如果不是直线还能简单的左旋和右旋吗?
如果插入后是一条直线那么经过一次左旋或者右旋可以解决问题。如果插入后是一条曲线那么一次左旋或者右旋解决不了任何问题。在折线的这种情况下我们需要左旋和右旋搭配使用。
先左旋父亲节点的下一个节点,然后在右旋父亲节点,旋转后需要重新调节平衡因子,50插入在30的左边和右边他的平衡因子是不同的,所以需要分情况讨论,上图是50插入在左边,如果插入的数字不是50是70,往30节点上插,那么70会插到30右边,看情况二如图:
如果是情况一调整后要修改 subL.bf = 0; subLR.bf = 0;parent.bf = 1;
如果是情况二要调整 subL.bf = -1; subLR.bf = 0;parent.bf = 0;
代码为:
private void rotateLR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
int bf = subLR.bf;
rotateLeft(parent.left);
rotateRight(parent);
if(bf == -1) {
subL.bf = 0;
subLR.bf = 0;
parent.bf = 1;
}else if(bf == 1){
subL.bf = -1;
subLR.bf = 0;
parent.bf = 0;
}
}
4.右左双旋
右左双旋和左右双旋类似我们直接看代码:
private void rotateRL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
int bf = subRL.bf;
rotateRight(parent.right);
rotateLeft(parent);
if(bf == 1) {
parent.bf = -1;
subR.bf = 0;
subRL.bf = 0;
}else if(bf == -1){
parent.bf = 0;
subR.bf = 1;
subRL.bf = 0;
}
}
总结:新节点插入后,假设以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
- pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋- pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋
即:pParent与其较高子树节点的平衡因子时同号时单旋转,异号时双旋转。
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
5.AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
- 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树 - 验证其为平衡树
每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)。
节点的平衡因子是否计算正确。
public void inorder(TreeNode root) {
if(root == null) return;
inorder(root.left);
System.out.print(root.val+" ");
inorder(root.right);
}
private int height(TreeNode root) {
if(root == null) return 0;
int leftH = height(root.left);
int rightH = height(root.right);
return leftH > rightH ? leftH+1 : rightH+1;
}
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
int leftH = height(root.left);
int rightH = height(root.right);
if(rightH-leftH != root.bf) {
System.out.println("这个节点:"+root.val+" 平衡因子异常");
return false;
}
return Math.abs(leftH-rightH) <= 1
&& isBalanced(root.left)
&& isBalanced(root.right);
}
测试用例:
AVLTree avlTree = new AVLTree();
int[] arr = {16,3,7,11,9,26,18,14,15};
for(int i=0;i< arr.length;i++){
avlTree.insert(arr[i]);
}
System.out.println(avlTree.isBalanced(avlTree.root));
}
6.AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2n。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
7.总代码
由于AVL树主要代码是插入和旋转,删除代码没有给出是因为AVL树不是我们学习的重点数据结构,红黑树才是。AVl树的删除和二叉搜素树的删除大概差不多,我们将在红黑树给出完整的代码。
class AVLTree {
static class TreeNode {
public int val;
public int bf;//平衡因子
public TreeNode left;//左孩子的引用
public TreeNode right;//右子树的引用
public TreeNode parent;//父亲节点的引用
public TreeNode(int val) {
this.val = val;
}
}
public TreeNode root;//根节点
public boolean insert(int val) {
TreeNode node = new TreeNode(val);
if(root == null) {
root = node;
return true;
}
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
if(cur.val < val) {
parent = cur;
cur = cur.right;
}else if(cur.val == val) {
return false;
}else {
parent = cur;
cur = cur.left;
}
}
//cur == null
if(parent.val < val) {
parent.right = node;
}else {
parent.left = node;
}
//
node.parent = parent;
cur = node;
// 平衡因子 的修改
while (parent != null) {
//先看cur是parent的左还是右 决定平衡因子是++还是--
if(cur == parent.right) {
//如果是右树,那么右树高度增加 平衡因子++
parent.bf++;
}else {
//如果是左树,那么左树高度增加 平衡因子--
parent.bf--;
}
//检查当前的平衡因子 是不是绝对值 1 0 -1
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;
}
private void rotateRL(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
int bf = subRL.bf;
rotateRight(parent.right);
rotateLeft(parent);
if(bf == 1) {
parent.bf = -1;
subR.bf = 0;
subRL.bf = 0;
}else if(bf == -1){
parent.bf = 0;
subR.bf = 1;
subRL.bf = 0;
}
}
/**
* 左右双旋
* @param parent
*/
private void rotateLR(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
int bf = subLR.bf;
rotateLeft(parent.left);
rotateRight(parent);
if(bf == -1) {
subL.bf = 0;
subLR.bf = 0;
parent.bf = 1;
}else if(bf == 1){
subL.bf = -1;
subLR.bf = 0;
parent.bf = 0;
}
}
/**
* 左单旋
* @param parent
*/
private void rotateLeft(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
parent.right = subRL;
subR.left = parent;
if(subRL != null) {
subRL.parent = parent;
}
TreeNode pParent = parent.parent;
parent.parent = subR;
if(root == parent) {
root = subR;
root.parent = null;
}else {
if(pParent.left == parent) {
pParent.left = subR;
}else {
pParent.right = subR;
}
subR.parent = pParent;
}
subR.bf = parent.bf = 0;
}
/**
* 右单旋
* @param parent
*/
private void rotateRight(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
parent.left = subLR;
subL.right = parent;
if(subLR != null) {
subLR.parent = parent;
}
//必须先记录
TreeNode pParent = parent.parent;
parent.parent = subL;
//检查 当前是不是就是根节点
if(parent == root) {
root = subL;
root.parent = null;
}else {
//不是根节点,判断这棵子树是左子树还是右子树
if(pParent.left == parent) {
pParent.left = subL;
}else {
pParent.right = subL;
}
subL.parent = pParent;
}
subL.bf = 0;
parent.bf = 0;
}
public void inorder(TreeNode root) {
if(root == null) return;
inorder(root.left);
System.out.print(root.val+" ");
inorder(root.right);
}
private int height(TreeNode root) {
if(root == null) return 0;
int leftH = height(root.left);
int rightH = height(root.right);
return leftH > rightH ? leftH+1 : rightH+1;
}
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
int leftH = height(root.left);
int rightH = height(root.right);
if(rightH-leftH != root.bf) {
System.out.println("这个节点:"+root.val+" 平衡因子异常");
return false;
}
return Math.abs(leftH-rightH) <= 1
&& isBalanced(root.left)
&& isBalanced(root.right);
}
}
public class Test {
public static void main(String[] args) {
AVLTree avlTree = new AVLTree();
int[] arr = {16,3,7,11,9,26,18,14,15};
for(int i=0;i< arr.length;i++){
avlTree.insert(arr[i]);
}
System.out.println(avlTree.isBalanced(avlTree.root));
}
}