数据结构 – 从二叉搜索树说到AVL树(二)之AVL树的操作与详解(Java)

  写在前:我在尽可能的写一篇能比较清晰且完整的讲完整个AVL树操作的文章,所有文字以及例图都是我一笔一划写出来的。由于AVL树的操作包含了查找,删除,插入操作,除了一些规律之外,一些处理细节,比如旋转操作,失衡时候的调整步骤等,除了死记硬背没有别的办法,所以我建议读者可以拿起笔,集中精神,跟着思路一口气看完,因为一些操作麻烦,走马观花的阅读势必会多花不必要的精力,不如一次性掌握起来,这也是我学习过程的一些体会,先结合例图理解过程,可以不看代码,免得增加理解负担。讲完我会把完整代码分享出来,同时也希望该篇文章能给予你一些帮助,多谢支持。

  本篇将要讲的是平衡二叉树,简称BBT(Balanced Binary Tree),其中的一种 — AVL树。 AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 “An algorithm for the organization of information” 中发表了它。

  为什么要有平衡二叉树呢, 同样都是二叉搜索树,使用以下三棵树搜索元素 1 有什么区别

            

  我们可以清晰看到从根节点出发,搜索元素 1 第一颗树要对比7次,而第二棵树要对比4次,第三棵树就只要3次。如果对所有的元素(1~7)全部搜索一次,然后计算对比平均次数,会发现第三种树是最少的,而且给你这七个数,你永远建立不出另外一种对比平均次数比第三棵树还少的二叉搜索树了。当节点数量不大的情况下可能区别不是很大,当如果数据量一大且这棵树刚好是第一种,那建立搜索树便失去了意义,所以我们的目的是要使我们的树变成类似的这种树,以使这棵树的效率达到最优。

  观察这三棵树可以发现第三棵树之所以效率最高,是因为这棵树比较”紧凑“,任何一个节点的左子树和右子树都差不多高(或者一样高),为了表示这个属性,AVL树的做法是给每一个节点增加一个属性,叫“平衡因子”,它的值等于当前节点的左子树高度减去右子树高度,当一个节点的平衡因子为-1或者1时,我们说这个节点是倾斜的,如果平衡因子为0,则这个节点是平衡的,倾斜和平衡都是AVL树的正常状态,但如果平衡因子小于-1或者大于1,则失去了平衡,此时要通过旋转来调整树的平衡。

  旋转分为两种:

  右旋:

  

  做法是以A节点为轴,节点A的左子树指向其左孩子B的右子树2,然后节点B的左子树指向节点A,然后原本节点A的父节点R对应的子树指向节点B,其他节点不作变化,这边便完成了左旋操作。

  相应的代码如下:以A点为轴进行右旋


    private void rotateRight(TreeNode pivot) {
        TreeNode leftChild = pivot.getLeft();
        TreeNode grandChildRight = leftChild.getRight();
        TreeNode parent = pivot.getParent();
        if (null == parent) {
            this.root = leftChild;
        } else if (pivot == parent.getLeft()) {
            parent.setLeft(leftChild);
        } else {
            parent.setRight(leftChild);
        }
        leftChild.setParent(parent);

        pivot.setLeft(grandChildRight);
        if (null != grandChildRight) {
            grandChildRight.setParent(pivot);
        }

        leftChild.setRight(pivot);
        pivot.setParent(leftChild);
    }

View Code

 

 

  左旋:

  

  左旋的操作跟右旋一样,但是结构是相反的,以节点A为轴,节点A的右子树指向其有孩子B的左子树2,然后节点B的左子树指向节点A,再使原节点A的父节点对应的子树指向节点B,其他节点不做改变。

  相应的代码如下:以A点为轴进行左旋


    private void rotateLeft(TreeNode pivot) {
        TreeNode rightChild = pivot.getRight();
        TreeNode grandChildLeft = rightChild.getLeft();
        TreeNode parent = pivot.getParent();
        if (null == parent) {
            // pivot node is root
            this.root = rightChild;
        } else if(pivot == parent.getLeft()) {
            parent.setLeft(rightChild);
        } else {
            parent.setRight(rightChild);
        }
        rightChild.setParent(parent);

        pivot.setRight(grandChildLeft);
        if (null != grandChildLeft) {
            grandChildLeft.setParent(pivot);
        }

        rightChild.setLeft(pivot);
        pivot.setParent(rightChild);
    }

View Code

 

  刚开始可能难吃透旋转的含义,可以拿笔纸然后自己写例子多画几遍就清晰了,这样旋转的目的是既不破坏一棵二叉搜索树的性质,又能使轴节点的平衡因子对应的降低或者升高到普通状态。

  

  AVL树的操作:

  AVL树节点的数据结构,比普通的二叉搜索树多了一个平衡因子属性,以下为树节点的数据结构

public class TreeNode {
    private int elem;
    private TreeNode left, right;
    private TreeNode parent;
    private int balanceFactor;
    public TreeNode(int elem) {
        this.elem = elem;
        this.balanceFactor = 0;
    }
}

 

  一、查找

  AVL树作为一棵二叉搜索树,其查找操作没有任何区别,可参考 数据结构 – 从二叉搜索树说到AVL树(一)之二叉搜索树的操作与详解(Java)

  二、插入

  AVL树的插入操作,除了作为一颗普通二叉搜索树的插入操作找到插入节点的位置之外,需要由插入节点开始上溯更新所有收到影响的节点的平衡因子,如果插入之后使树失去平衡(即某个节点的平衡因子为-2或者2),则需要对树进行旋转调整,一个AVL树失去平衡的情况其实有很多,但插入的操作是在插入前AVL树本身就已经平衡这个条件下,失衡情况主要分为以下四种:

  第一个L(R)表示当前节点的左(右)子树失去平衡,即轴节点的平衡因子为2(-2),第二个L(R)表示轴节点的左(右)子树是向左(右)倾斜的,即左(右)子树的平衡因子为1(-1)。

  1. LL 型

  以失去平衡的节点为parent,parent左节点为left,处理方式是以parent为轴做右旋操作,旋转操作没有技巧,多写几遍直到看到一个树就能在脑海想到旋转之后的样子。

  平衡因子调整:

  旋转之后做平衡因子调整,LL型的调整规则是把parent和left节点的平衡因子设置为0,其他节点保持不变。

   

 

  代码如下:


    private void rotateLLFix(TreeNode parent) {
        TreeNode left = parent.getLeft();
        rotateRight(parent);
        // update the balance factor
        parent.setBalanceFactor(0);
        left.setBalanceFactor(0);
    }

View Code

 

  2. LR 型

  LR型的旋转操作麻烦一些,以轴为parent,parent的左节点为left,left的右节点为grandchild,以下图第一行三个图为例,如果直接对着三个图做右转操作,会发现这么做并不能使这课子树回复平衡。必须先把LR型转化为LL型,做法是先以left为轴做左转操作,得到下图第二行的结果展示,接着再以parent为轴做右转操作,这样才使这棵子树回复到平衡状态。

  平衡因子调整:

    LR型的平衡因子调整根据原grandchild的平衡因子分为三种情况:

    ① 如果grandchild原平衡因子为+1,则parent的平衡因子设置为-1,left的平衡因子设置为0

    ② 如果grandchild原平衡因子为0, 则parent和left的平衡因子设置为0

    ③ 如果grandchild原平衡因子为-1,则parent的平衡因子设置为0, left的平衡因子设置为+1

    以上三种情况的grandchild平衡因子皆设置为0, 其他节点没有变化

  代码如下:


    private void rotateLRFix(TreeNode parent) {
        TreeNode left = parent.getLeft();
        TreeNode grandchild = parent.getRight();
        rotateLeft(left);
        rotateRight(parent);
        // update the balance factor
        if (0 == grandchild.getBalanceFactor()) {
            parent.setBalanceFactor(0);
            left.setBalanceFactor(0);
        } else if (-1 == grandchild.getBalanceFactor()) {
            parent.setBalanceFactor(0);
            left.setBalanceFactor(-1);
        } else {
            left.setBalanceFactor(0);
            parent.setBalanceFactor(-1);
        }
        grandchild.setBalanceFactor(0);
    }

View Code

 

  3. RR 型

  RR型的调整规则与LL型的调整规则镜面对称的, parent设置不变,把parent的右节点设置为right,然后以parent为轴做左旋操作

  平衡因子调整:

  RR型的调整规则是把parent和right节点的平衡因子设置为0,其他节点保持不变。

  

  代码如下:


    private void rotateRRFix(TreeNode parent) {
        TreeNode right = parent.getRight();
        rotateLeft(parent);
        parent.setBalanceFactor(0);
        right.setBalanceFactor(0);
    }

View Code

 

  4. RL 型

   RL型和LR型同样道理也是镜面对称的,把parent的右节点设置为right,right节点的左节点设置为grandchild,处理方式是先以right节点为轴做右旋操作,使之转化为RR型,然后再以parent位轴做左旋操作,即可恢复平衡。

  平衡因子调整:

  同样根据grandchild的平衡因子分为三种情况

    ① 如果grandchild原平衡因子为+1,则parent的平衡因子设置为0,right的平衡因子设置为-1

    ② 如果grandchild原平衡因子为0, 则parent和right的平衡因子设置为0

    ③ 如果grandchild原平衡因子为-1,则parent的平衡因子设置为+1, right的平衡因子设置为0

    以上三种情况的grandchild平衡因子皆设置为0, 其他节点没有变化

  

  代码如下:


    private void rotateRLFix(TreeNode parent) {
        TreeNode right = parent.getRight();
        TreeNode grandchild = right.getLeft();
        rotateRight(right);
        rotateLeft(parent);
        if (0 == grandchild.getBalanceFactor()) {
            parent.setBalanceFactor(0);
            right.setBalanceFactor(0);
        } else if (-1 == grandchild.getBalanceFactor()) {
            parent.setBalanceFactor(1);
            right.setBalanceFactor(0);
        } else {
            parent.setBalanceFactor(0);
            right.setBalanceFactor(-1);
        }
        grandchild.setBalanceFactor(0);
    }

View Code

 

  讨论完上面几种调节失衡情况的细节,接下来讨论怎么在插入节点时候找到这个需要调节的节点。

  1. 插入一个新节点到某个节点(父节点)的左子树,此时父节点的平衡因子在原来的基础上加上1,而后分为两种情况:

    ① 父节点此时的平衡因子为0。则说明本来父节点的平衡因子为-1,父节点以下的子树高度并没有发生变化,纵观整棵树,插入这个新节点除了影响了父节点的平衡因子之外,对其他节点均没有印象,此时,只需要调整父节点的平衡之后,插入操作结束

    ② 父节点此时的平衡因子为1,则说明以父节点以下的这课子树高度增加了1,影响到了从这个新节点开始向上到根节点路径的所有节点平衡因子,需要从节点开始向上调整所有节点的平衡因子知道根节点,如果路径上有节点的平衡因子调整后为2或者-2,则根据情况对节点进行以上所讲平衡调整,使该节点的平衡因子回复到跟原来一样,然后从这个节点开始向上到根节点路径的节点皆不再受到影响,插入操作结束。为什么对调整后的节点向上都不会有影响呢,因为本来这种情况下新的节点会使这个子树高度增加1,所以通过旋转调整让这颗子树的高度又减少了1,所以对其他节点来说,这个子树高度没有变化,所以便没有影响了。在这个特点上需要和下文即将说到的删除节点后调整平衡情况作出对比。

  2. 插入一个新节点到某个节点(父节点)的右子树,此时父节点的平衡因子在原来的基础上减去1,而后分为两种情况:

    ① 父节点此时的平衡因子为0。经过上面插入左子树的情况讨论,此处不再赘述。

    ② 父节点此时的平衡因子为-1,同理上溯到根节点,上溯路径如果有节点的平衡因子为-2或者2,则对该节点进行平衡调整,同样不再继续影响该节点以上路径节点,插入操作结束。

 

  详细代码如下:


    private boolean insertNode(TreeNode parent, TreeNode node) {
        if (parent.getElem() == node.getElem()) {
            return false;
        } else if (parent.getElem() > node.getElem()) {
            if (null == parent.getLeft()) {
                parent.setLeft(node);
                node.setParent(parent);
                insertFixUp(node);
                return true;
            } else {
                return insertNode(parent.getLeft(), node);
            }
        } else {
            if (null == parent.getRight()) {
                parent.setRight(node);
                node.setParent(parent);
                insertFixUp(node);
                return true;
            } else {
                return insertNode(parent.getRight(), node);
            }
        }
    }

View Code

  其中的 insertFixUp(node); 指从node开始上溯到根节点调整该路径的节点平衡因子,并做必要的旋转操作,代码如下:


    private void insertFixUp(TreeNode node) {
        TreeNode parent = node.getParent();
        while (null != parent) { // track to root when parent is not null
            if (node == parent.getLeft()) {
                parent.setBalanceFactor(parent.getBalanceFactor() + 1);
            } else {
                parent.setBalanceFactor(parent.getBalanceFactor() - 1);
            }
            if (0 == parent.getBalanceFactor()) {
                break;
            }
            if (-2 == parent.getBalanceFactor() || 2 == parent.getBalanceFactor()) {
                if (2 == parent.getBalanceFactor()) {
                    TreeNode left = parent.getLeft();
                    if (-1 == left.getBalanceFactor()) {
                        rotateLRFix(parent);
                    } else {
                        rotateLLFix(parent);
                    }
                } else {
                    TreeNode right = parent.getRight();
                    if (1 == right.getBalanceFactor()) {
                        rotateRLFix(parent);
                    } else {
                        rotateRRFix(parent);
                    }
                }
                break;
            }
            node = parent;
            parent = node.getParent();
        }
    }

View Code

 

  三、删除

  最后一个操作,操作方式也跟普通的BST一样,可参考: 数据结构 – 从二叉搜索树说到AVL树(一)之二叉搜索树的操作与详解(Java)(删除的节点只有左子树,删除的节点只有右子树,删除的节点是叶子,如果删除的节点同时拥有左右子树也可以转化为以上三种情况)。

  不同的是把该节点删除之后的平衡调整操作。

  讨论删除节点后的情况:

  1. 删除的节点是其父节点的左子树,用删除节点的左子树或者右子树代替被删除的节点,若删除的节点是叶子节点,则直接删除,删除后父节点的平衡因子在原来的基础上减去1,而后可以分为以下两种:

    ① 删除之后父节点平衡因子为-1,则说明原本父节点的左右子树是高度一致的,删除掉这个节点没有影响了从父节点下来的这棵子树的整体高度,影响的只是父节点的平衡因子,调整父节点的平衡因子,删除操作结束

    ②删除之后父节点平衡因子为0,说明父节点的平衡因子从1变成0,从父节点下来的这棵子树高度变低了,则需要从父节点开始上溯直到根节点,调整路径上所有节点的平衡因子,如果经过的节点调整之后平衡因子为-2或者2,则做对应的平衡调整操作,使其平衡因子回复到原来的情况。然后,此处就是前面所说的和插入操作旋转调整之后不同的地方,因为插入一个新节点而导致需要旋转调整其实是因为某棵子树的高度增加了,此时旋转可以把高度又降低到原来的状态,所以调整后对父节点以上的所有节点都没有影响了,因为高度已经恢复了。但删除操作里面,当调整平衡之后,其实原本失去平衡的节点以下子树的高度已经比原来的高度减少了1(因为删除的节点必然不是这颗子树最大深度?),所以失去平衡的节点调整之后也影响了父节点以上的节点平衡因子,所以必须一直上溯直到根节点为止。

  2. 删除的节点是其父节点的右子树,用删除节点的左子树或者右子树代替被删除的节点,若删除的节点是叶子节点,则直接删除,删除之后父节点的平衡因子在原来的基础上加上1,同样可两种情况,与上面的情况是镜像对称的,此处不再赘述。

    下面用图例来说明这个过程:

    

    如果要删除值为1的节点,删除之后调整平衡到有图,可注意到由值为5的节点的左子树其实高度比原本小了1,所以此时需要继续向上调整平衡因子并做必要的旋转

    

    往上找发现值为5的节点也失去平衡,旋转调整后,发现5是根节点,删除操作结束。若值为5的节点不是根节点,可以发现该子树的高度比原来也少了1,同样需要继续上溯。

  

  删除的代码以及删除节点之后的调节代码如下:


    public boolean delete(int elem) {
        if (null == this.root) {
            return false;
        } else {
            TreeNode node = this.root;
            // find out the node need to be deleted
            while (null != node) {
                if (node.getElem() == elem) {
                    deleteNode(node);
                    return true;
                } else if (node.getElem() > elem) {
                    node = node.getLeft();
                } else {
                    node = node.getRight();
                }
            }
            return false;
        }
    }

    private void deleteNode(TreeNode node) {
        TreeNode parent = node.getParent();
        if (null == node.getLeft() && null == node.getRight()) {
            if (null == parent) {
                this.root = null;
            } else if (node == parent.getLeft()) {
                parent.setLeft(null);
                parent.setBalanceFactor(parent.getBalanceFactor() - 1);
            } else {
                parent.setRight(null);
                parent.setBalanceFactor(parent.getBalanceFactor() + 1);
            }
            deleteFixUp(parent);
        } else if (null == node.getLeft()) {
            TreeNode right = node.getRight();
            if (null == parent) {
                this.root = right;
            } else if (node == parent.getLeft()) {
                parent.setLeft(right);
                parent.setBalanceFactor(parent.getBalanceFactor() - 1);
            } else {
                parent.setRight(right);
                parent.setBalanceFactor(parent.getBalanceFactor() + 1);
            }
            if (null != right) {
                right.setParent(parent);
            }
            deleteFixUp(parent);
        } else if (null == node.getRight()) {
            TreeNode left = node.getLeft();
            if (null == parent) {
                this.root = left;
            } else if (node == parent.getLeft()) {
                parent.setLeft(left);
                parent.setBalanceFactor(parent.getBalanceFactor() - 1);
            } else {
                parent.setRight(left);
                parent.setBalanceFactor(parent.getBalanceFactor() + 1);
            }
            if (null != left) {
                left.setParent(parent);
            }
            deleteFixUp(parent);
        } else {
            TreeNode pre = node.getLeft();
            while (null != pre.getRight()) {
                pre = pre.getRight();
            }
            TreeUtils.swapTreeElem(pre, node);
            deleteNode(pre);
        }
    }

    /**
     * fix up tree from node after delete node
     * @param node
     */
    private void deleteFixUp(TreeNode node) {
        if (null == node || -1 == node.getBalanceFactor() || 1 == node.getBalanceFactor()) {
            return;
        } else {
            TreeNode parent = node.getParent();
            boolean isLeft = null != parent && parent.getLeft() == node ? true : false;
            if (-2 == node.getBalanceFactor()) {
                TreeNode right = node.getRight();
                if (-1 == right.getBalanceFactor()) {
                    rotateRRFix(node);
                } else {
                    rotateRLFix(node);
                }
            } else  if (2 == node.getBalanceFactor()) {
                TreeNode left = node.getLeft();
                if (1 == left.getBalanceFactor()) {
                    rotateLLFix(node);
                } else {
                    rotateLRFix(node);
                }
            }
            if (null != parent) {
                if (isLeft) {
                    parent.setBalanceFactor(parent.getBalanceFactor() - 1);
                } else {
                    parent.setBalanceFactor(parent.getBalanceFactor() + 1);
                }
                // up tracking until root
                deleteFixUp(parent);
            }
        }
    }

View Code

   

  最后我们就来测试一下这个删除操作吧。

  构建上图删除节点前的树如下图:

  

  这个图要顺时针旋转90°来看,中括号表示该节点的平衡因子。

  删除值为1的节点,删除成功

--------------------------------------------------------------
1. insert
2. delete
3. search
4. print
5. exit
->2 1
->delete success

  删除后的树结构为:

  

  和我们意料之中一样,问题不大。

  整个AVL树除了旋转操作需要死记硬背之外,其他的操作只要懂得这么操作的原理,代码实现起来都是相对比较简单。

  至此,所有AVL树的操作均全部完成,如果不妥之处欢迎大家提出斧正。

 

 

 

 

 

 

 

 

   尊重知识产权,引用转载请标明出处并通知原作者