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

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

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

  为何要有平衡二叉树呢, 一样都是二叉搜索树,使用如下三棵树搜索元素 1 有什么区别数据结构

  

  能够发现第三棵树的搜索效率是最高的,任何一个节点的左子树和右子树都差很少高(或者同样高),为了表示这个属性,AVL树的作法是给每个节点增长一个属性,叫“平衡因子”(balance factor),它的值等于当前节点的左子树高度减去右子树高度,当一个节点的平衡因子为-1或者1时,咱们说这个节点是倾斜的,若是平衡因子为0,则这个节点是平衡的,倾斜和平衡都是AVL树的正常状态,但若是平衡因子小于-1或者大于1,则表明失去了平衡,此时要经过旋转来调整树的平衡。ide

  旋转分为两种 —— 右旋和左旋:学习

  右旋:测试

  

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

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

    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

 

 

  左旋:code

  

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

  相应的代码以下:以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;
    }
}

 

  1、查找

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

  2、插入

  根据普通二叉搜索树的插入步骤插入一个新的节点以后,对整棵树受到影响的节点更新其平衡因子,更新的规则在讨论完失衡状况后进行讲解,失衡状况主要分为如下四种,以及调节方式以下,调节以后须要更新某些节点的平衡因子,也是至关重要的部分:

  *第一个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

 

  3、删除

  最后一个操做,操做方式也跟普通的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树的操做均所有完成,若是不妥之处欢迎你们提出斧正。

 

 

 

 

 

 

 

 

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

相关文章
相关标签/搜索