Java实现AVL树的添加和删除

 

前言

AVL树的基本性质

AVL树节点设计

插入时会出现什么问题?

2为3的左节点,1为2的左节点

2为1的右节点,3为2的右节点

1为3的左节点,2为1的右节点

3为1的右节点,2为3的左节点

AVL树的插入

AVL树的判断

AVL树的删除

后记


前言

        前面写过两篇关于二叉搜索树的博文,但是它不具有平衡性,最差情况时,会退化成链表,查找的效率会降至O(n)。为了避免这样的情况发生,有人就在原始二叉搜索树的基础上,设置了相应条件,使其变为平衡的二叉搜索树,保留它高效率搜索的特性。关于AVL树的文章很多,很多都是数据结构和算法书上的东西。在我刚开始学习AVL树时,我也是按部就班的根据书上的算法进行学习,照着写很流畅,可是过两天就又忘了,出现这种情况,只能怪我对它了解的太少,没有学透它。为了真正掌握它,我让自己彻底忘记书上关于AVL树的算法,只通过它的性质,从头推导它,亲身体验在实现过程中遇到的问题,这些问题应该怎样解决,最终得到属于自己的算法,因此这篇博文侧重点在AVL树算法的推理和实现。

AVL树的基本性质

        AVL树的性质非常简单,就一句话:在插入和删除过程中,AVL树中任何节点的两个子树高度的最大差值为1。具体的细节可以参照维基百科:AVL tree

AVL树节点设计

        AVL树需要保证任何节点的两个子树高度的最大差值为1,相比于二叉搜索树的树节点,AVL树节点需要在它的基础增加一个表示节点高度的字段。因为添加和删除都发生在树的末端,因此树的高度应该从下向上递增,根节点表示的节点高度最大。我们把空节点的高度记作0,叶节点的高度记作1,因此每一个插入的新节点,其高度都为1,然后其父节点在子节点的基础上(取左右子树的最大高度)加一即可。树节点类的具体代码如下:

class Node<E extends Comparable<E>>{
    public E value;
    public Node<E> parent;
    public Node<E> left;
    public Node<E> right;
    public int height;
    public Node(E value, Node<E> parent){
        this.value = value;
        this.parent = parent;
        this.height = 1;
    }
    public String toString(){
        return value + "(" + height + ")"; 
    }
}

        获取节点高度的函数如下:

private int height(Node<E> node){
    return node == null ? 0 : node.height;
}

插入时会出现什么问题?

        AVL树的插入还是按照以前的二叉搜索树的插入方法实现,但是在插入过程中,会出现节点的两个子树高度的最大差值大于1的情况。如果AVL树规则被破坏,则两子树的高度差为2,具体的情况如下所示。

        3节点的左子树高度为2,右子树高度为0,两子树高度差为2。因为2节点可以为3节点的左右孩子节点,2节点就有两种位置情况,1节点可以为2节点的左右孩子节点,1节点也有两种位置情况,所有这种插入异常一共就有四种情况,则AVL树在插入时候,有四种破坏平衡的情况需要解决。

2为3的左节点,1为2的左节点

        这种情况的具体图示如下所示:

        左边的情况,3节点的左右子树高度差为2,要想平衡左右子树,那必须让3的右子树的高度加1,可是从哪里借一个节点呢?从左子树借,因为左子树的值都小于3,不能放在3的右子树上,因此只能把父节点3借来,使AVL树达到平衡。

        对于左边的形式,我们只需要将3设置为2的右节点即可,因为3是向右移动,所以这种解决方式一般叫右旋。上面的图像是这种形式的最简单表示,因为简单,所有很多东西都没考虑,第一、3节点是否有父节点,如果存在,则该父节点应该和2节点联系起来;第二、2节点是否存在右节点,如果存在,则2的右节点应该和3节点联系起来。

        下面我们就将所有情况都加上去,其图示如下所示:

        上面的图可能会有一些不严谨,比如7节点的高度不一定会变化等,但是我们的重点是如何理解那个右旋,对于左图,我们根据括号里面的高度,能够很快知道,节点5不平衡。按照我们前面说的,将5右移到3的右节点,把3的右节点移到5的左节点即可。总结起来就是:

        1、5的左节点为3的右节点,如果4节点存在,则4的父节点为5节点。

        2、3的右节点为5节点,5的父节点为3节点,3的父节点为7。

        3、如果5的父节点7不存在,则根节点为3节点,若存在,则7节点的左节点为3节点。

        4、将5节点的高度设置为左右子树最大高度加1,3节点高度也设置为其左右子树最大高度加1。

        将上面的总结转换为代码如下:

private void rotateRight(Node<E> node){
    assert node.left != null;
    Node<E> left = node.left;
    node.left = left.right;
    if(left.right != null){
        left.right.parent = node;
    }
    Node<E> parent = node.parent;
    node.parent = left;
    left.right = node;
    left.parent = parent;
    if(parent == null){
        root = left;
    }else if(node == parent.left){
        parent.left = left;
    }else{
        parent.right = left;
    }
    node.height = Math.max(height(node.left), height(node.right)) + 1;
    left.height = Math.max(height(left.left), height(left.right)) + 1;
}

2为1的右节点,3为2的右节点

        这种情况的具体图示如下所示:

        这种情况和前面的情况非常类似,只是换成了右节点罢了。我们仍然借的是父节点1来填充节点2的左节点,来实现树的平衡。对于左边的形式,我们只需要将1设置为2的左节点即可,因为1是向左移动,所以这种解决方式一般叫左旋。上面的图像是这种形式的最简单表示,因为简单,所有很多东西都没考虑,第一、1节点是否有父节点,如果存在,则该父节点应该和2节点联系起来;第二、2节点是否存在左节点,如果存在,则2的左节点应该和3节点联系起来。

        下面我们就将所有情况都加上去,其图示如下所示:

        上面的图可能会有一些不严谨,比如7节点的高度不一定会变化等,但是我们的重点是如何理解那个左旋,对于左图,我们根据括号里面的高度,能够很快知道,节点4不平衡。按照我们前面说的,将2右移到4的右节点,把4的右节点移到2的左节点即可。总结起来就是:

        1、2的右节点为4的左节点,如果3节点存在,则3的父节点为2节点。

        2、4的左节点为2节点,2的父节点为4节点,4的父节点为7。

        3、如果2的父节点7不存在,则根节点为4节点,若存在,则7节点的左节点为4节点。

        4、将2节点的高度设置为左右子树最大高度加1,5节点高度也设置为其左右子树最大高度加1。

        将上面的总结转换为代码如下:

private void rotateLeft(Node<E> node){
    assert node.right != null;
    Node<E> right = node.right;
    node.right = right.left;
    if(right.left != null){
        right.left.parent = node;
    }
    Node<E> parent = node.parent;
    node.parent = right;
    right.left = node;
    right.parent = parent;
    if(parent == null){
        root = right;
    }else if(node == parent.left){
        parent.left = right;
    }else{
        parent.right = right;
    }
    node.height = Math.max(height(node.left), height(node.right)) + 1;
    right.height = Math.max(height(right.left), height(right.right)) + 1;
}

1为3的左节点,2为1的右节点

        这种情况的具体图示如下所示:

        对于最左边的图,如果想使其达到平衡,那么我们需要使1成为2的左子节点,3成为2的右子节点,因此它的平衡需要分两步来实现,首先把节点1转换节点2的左节点,就如同中间图所示,如何实现呢?1向左运动变成2的左节点,这种操作不就是前面的左旋吗,因此我们对节点1进行左旋,即可。如何将节点3转换节点2的右节点呢?对于中间的图,将3向右运动变成2的右节点,这种操作不就是前面的右旋吗,因此我们对节点3进行右旋,即可。因为前面的左、右旋都把所有情况考虑了,这种情况的总结就是:

        1、首先对节点1进行左旋。

        2、最后对节点3进行右旋。

        将上面的总结转换为代码如下:

public void rotateLeftRight(Node<E> node){
    rotateLeft(node.left);
    rotateRight(node);    
}

3为1的右节点,2为3的左节点

        这种情况的具体图示如下所示:

  这种情况和上面几乎类似,其原理也差不多,就不多赘述。它的的总结就是:首先对节点3进行右旋,最后对节点1进行左旋。具体代码如下:

private void rotateRightLeft(Node<E> node){
    rotateRight(node.right);
    rotateLeft(node);
}

AVL树的插入

        前面我们介绍了,在插入过程中,可能出现的一些问题,并通过相应的节点旋转来实现平衡。接下来我们具体说说,插入一个新的节点,如何平衡整个AVL树。

        我们在平衡过程中,有两个问题需要解决,第一个是插入后会影响到父节点的高度,父节点的高度需要重新计算。第二插入后造成AVL树不平衡,需要节点的左右旋转保持树的平衡。

        对于第一个问题,当我们通过二叉搜索树的插入方法,插入一个高度为1的新节点时,它的父节点的高度并没有因为它的到来,而直接加1,而是通过它的左右节点的最大高度加1来决定。如果通过计算,左右节点的最大高度加1的值和父节点的高度值相同,这说明此次插入并没有破坏AVL树的平衡,直接退出,如果不相等,则父节点的高度等于该值。

        总结如下:1、如果父节点的左右子树高度的最大值加1的值h等于父节点的高度,AVL树平衡,直接退出程序。

                          2、如果父节点的左右子树高度的最大值加1的值h大于父节点的高度,父节点高度等于该值h,继续执行。

        对于第二个问题,当父节点的左右子节点的高度差等于2,这说明此次插入破坏了AVL树的平衡。首先,我们应该判断是左子树的高度大,还是右子树的高度大。

        如果是左子树高度比右子树的高度大,则说明被插入的节点在左子树,此时我们再判断被插入的节点在左子树的哪边,可以通过两种方式判断。一种是,利用新节点的值v1和左节点的值v2比较,如果v1>v2,则新节点被插入在左子树的右边,如果v1<v2,则新节点被插入在左子树的左边;另一种是比较左子树的左右子树高度,如果左边高,则新节点被插入在左子树的左边,如果右边高,则新节点被插入在左子树的右边。如果新节点被插入在左子树的左边,则利用右旋来平衡AVL树。如果新节点被插入在左子树的右边,则利用右左旋来平衡AVL树。

        如果是右子树高度比左子树的高度大,其处理方式和上面一样,只是旋转方向刚好相反,具体就不赘述了。

        这里我们需要考虑一件事情,经过旋转平衡后的AVL树,是否还需要继续平衡?当插入一个新的节点时,父节点的高度可能加1,也可能不变,如果不变,就直接退出了,如果变化,就可能造成AVL树的不平衡,出现不平衡,我们采用的是节点的左右旋转来维稳。如果我们仔细观察这四种旋转方式,会发现旋转完成后,旋转部分的子树的高度减1。插入节点造成的加1和旋转维稳的减一刚好抵消,使得被插入子树的高度保持不变,又因为AVL树左右子树都符合AVL树的规则,因此,在插入过程中,AVL树最多只需要旋转两次(左旋和右旋分别记作一次)即可实现平衡。

        总结如下:1、判断左子树高度和右子树高度差是否等于2,等于则需要进行平衡,不等于,不处理。

                          2、如果树不平衡,判断左右子树高度大小,再使用compareTo方法或者高度判断方法决定旋转方式。

                          3、平衡处理后,直接退出程序,因为AVL一次维稳操作即可保证整个AVL树的平衡。

        具体代码如下:

public boolean add(E e){
    if(e == null){
        return false;
    }
    Node<E> node = root;
    if(node == null){
        root = new Node<>(e, null);
        size++;
        return true;
    }
    Node<E> parent = null;
    int cmp = 0;
    do{
        parent = node;
        cmp = e.compareTo(node.value);
        if(cmp > 0){
            node = node.right;
        }else if(cmp < 0){
            node = node.left;
        }else{
            return false;
        }
    }while(node != null);
    Node<E> newNode = new Node<>(e, parent);
    if(cmp > 0){
        parent.right = newNode;
    }else{
        parent.left = newNode;
    }
    fixAfterInsertion(parent);
    size++;
    return true;
}

private void fixAfterInsertion(Node<E> parent){
    do{
        int leftHeight = height(parent.left);
        int rightHeight = height(parent.right);
        if(Math.abs(leftHeight - rightHeight) == 2){
            if(leftHeight > rightHeight){  // Add to the left subtree
                Node<E> left = parent.left;
                if(height(left.left) > height(left.right)){
                    rotateRight(parent);
                }else{
                    rotateLeftRight(parent);
                }
            }else{ // Add to the right subtree
                Node<E> right = parent.right;
                if(height(right.right) > height(right.left)){
                    rotateLeft(parent);
                }else{
                    rotateRightLeft(parent);
                }
            }
            break;
        }
        int newHeight = Math.max(leftHeight, rightHeight) + 1;
        if(parent.height == newHeight){
            break;
        }else{
            parent.height = newHeight;
        }
        parent = parent.parent;
    }while(parent != null);
}

        其中的关于二叉搜索树插入部分的函数可以参考我的这篇博文:二叉搜索树的插入

AVL树的判断

        通过广度优先搜索遍历AVL节点,接着判断节点的左右高度差,若高度差大于等于2则不为AVL树。具体代码如下:

public boolean isValid(){
    Node<E> node = root;
    if(node == null){
        return true;
    }
    ArrayDeque<Node<E>> nodeDeque = new ArrayDeque<>();
    nodeDeque.addFirst(node);
    while(!nodeDeque.isEmpty()){
        node = nodeDeque.removeLast();
        if(Math.abs(height(node.left) - height(node.right)) >= 2){
            return false;
        }
        if(node.left != null){
            nodeDeque.addFirst(node.left);
        }
        if(node.right != null){
            nodeDeque.addFirst(node.right);
        }
    }
    return true;
}

AVL树的删除

        AVL树的删除部分和二叉搜索树的删除方法一模一样,但是删除节点后,可能使其子树节点的高度减一,进而造成左右子树节点的高度差为2,从而破坏AVL树的稳定性。和前面插入一样,我们仍然需要考虑删除节点后的两个问题,一个是父节点的高度如何改变,二是AVL树稳定性被破坏后如何维稳。

        对于第一个问题,当我们通过二叉搜索树的删除方法,删除一个高度为1的新节点时,它的父节点的高度并没有因为它的删除,而直接减1,而是由它的左右节点的最大高度加1来决定。如果通过计算,左右节点的最大高度加1的值和父节点的高度值相同,这说明此次删除并没有破坏AVL树的平衡,直接退出,如果不相等,则父节点的高度等于该值。

        总结如下:1、如果父节点的左右子树高度的最大值加1的值h等于父节点的高度,AVL树平衡,直接退出程序。

                          2、如果父节点的左右子树高度的最大值加1的值h大于父节点的高度,父节点高度等于该值h。

        对于第二个问题,当父节点的左右子节点的高度差等于2,这说明此次插入破坏了AVL树的平衡。首先,我们应该判断是左子树的高度大,还是右子树的高度大。      

        如果是左子树高度比右子树的高度大,则说明被删除的节点在右子树,说明左子树需要维稳操作,接着我们再判断左子树的哪个子树高度过高。因为被删除的节点在右子树,所以这里不能像插入一般可以通过两种方式判断,只能比较左子树的左右子树高度来进行判断。如果左边高,则左边子树高度过高,需要左旋来降低高度,如果右边高,则说明右边子树高度过高,需要左右旋来降低高度,如果两个子树高度一样怎么办呢?

        在前面插入时,失稳的左右子树高度不可能相同,所以,我们没有提到这个问题。但是在删除时,却可能出现这个问题。这里我们先假设使用左旋来实现维稳,具体图例如下:

        由上可知,左旋能够实现这种情况的维稳。

        接下来是左右旋操作的图例,如下所示:

        由上可知,左右旋虽然将4节点维稳了,但是3节点确失稳了。为什么会这样呢?在前面分析左右旋时,我们知道,假设对节点6进行左右旋,就是把4提到6的位置,然后自断左右子树分给父节点(3)和祖父节点(6),但是却可能自己并没有左或者右节点,这样就会让原先的父节点或者祖父节点的某个子树高度为0,使得旋转后的祖父或者父节点的左右子树的高度差等于2。因为4节点没有左节点,所以一定会在旋转后,父节点3没有右节点,出现平衡破坏的情况。至于没有右节点的情况,就在下面进行介绍。为什么单纯的左旋没有造成这样的情况呢,只是因为左旋是将3换到6的位置,然后将右子树给6,因为3节点的左右节点都存在,所以绝对不会让6节点旋转后缺失左节点,不会出现平衡破坏的情况,故左边子树高度和右边子树高度相同时,使用左旋操作来实现维稳。

         如果是右子树高度比左子树的高度大,则说明被删除的节点在左子树,说明右子树需要维稳操作,具体的操作和前面的情况相同,只是说明一下,右子树的左边子树高度和右边子树高度相同时,应该怎么处理。

        首先采用右旋处理,具体图示如下所示:

        由上可知,右旋能够实现这种情况的维稳。

        接下来是右左旋操作的图例,如下所示:

        这种情况就是前面说的,没有右节点的情况。使用右左旋,会使原先的父节点5缺少左节点,出现失稳现象。它的详细解释,可以参考前面的介绍,原理类似。

        删除是不是和插入一样,只需要一次维稳,整棵AVL树就都平衡了呢?其实不是,因为删除一个节点,父节点的高度可能减1,而维稳操作,也可能使高度减一,因此,一次维稳可能并不能使AVL树完全平衡,需要继续判断,可能最终追溯到根节点。

        总结如下:1、判断左子树高度和右子树高度差是否等于2,需要进行平衡,不等于,不处理,跳出操作。

                          2、如果树不平衡,判断左右子树高度大小,判断失衡的位置,选择合适的旋转方式。

                          3、平衡处理后,将下次操作节点指向祖父节点,再次判断平衡性。

        具体代码如下:

public boolean remove(Object e){
    Node<E> replaced = getNode(e);
    if(replaced == null){
        return false;
    }else{
        --size;
    }
    Node<E> removed = replaced;
    if(replaced.left != null && replaced.right != null){
        if(height(replaced.left) > height(replaced.right)){
            removed = predecessor(replaced);
        }else{
            removed = successor(replaced);
        }
    }
    Node<E> moved = null;
    if(removed.left != null){
        moved = removed.left;
    }else{
        moved = removed.right;
    }
    Node<E> parent = removed.parent;
    if(moved != null){
        moved.parent = parent;
    }
    if(parent == null){
        root = moved;
        return true;
    }else if(removed == parent.left){
        parent.left = moved;
    }else{
        parent.right = moved;
    }
    if(replaced != removed){
        replaced.value = removed.value;
    }
    fixAfterDeletion(parent);
    return true;
}

private void fixAfterDeletion(Node<E> parent){
    do {
        int leftHeight = height(parent.left);
        int rightHeight = height(parent.right);
        if(Math.abs(leftHeight - rightHeight) == 2){
            if(leftHeight > rightHeight){ 
                Node<E> left = parent.left;
                if(height(left.right) > height(left.left)){ 
                    rotateLeftRight(parent);
                }else {
                    rotateRight(parent);
                }
             }else { 
                Node<E> right = parent.right;
                if(height(right.left) > height(right.right)){ 
                    rotateRightLeft(parent);
                }else { 
                    rotateLeft(parent);
                }
            }
            parent = parent.parent.parent; // Grandparent node
            continue;
        }
        int nextHeight = Math.max(leftHeight, rightHeight) + 1;
        if(parent.height == nextHeight){
            break;
        }else {
            parent.height = nextHeight;
        }
        parent = parent.parent;
	} while (parent != null);
}

        其中的关于二叉搜索树删除部分的函数可以参考我的这篇博文:二叉搜索树的删除

后记

        AVL树不管删除还是插入,其核心思想都是为了满足左右子树的高度差小于2的条件,我们的维稳处理也必须从这方面入手。其实可以通过平衡因子(-1、0、1)来处理平衡,但是我觉得高度更直观些,所以就使用高度属性来作为维稳基础。虽然算法书上很多介绍AVL树的代码,关于它的博文也纷繁复杂,但是还是希望自己能够真正掌握AVL树的核心,能通过自己的分析写出相应算法,不然你学的永远是别人咀嚼剩下的东西。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页