美文网首页
自己动手写数据结构之平衡二叉树

自己动手写数据结构之平衡二叉树

作者: 逍遥白亦 | 来源:发表于2021-01-02 23:03 被阅读0次

    平衡二叉树

    我们知道,对于二叉查找树,树的层数越少,查找数据的平均查找时间就会越少。二叉排序树在很多情况下都不能保证树的高度是最优的,例如在极端情况,如果插入排序树的数据是有序的,那么二叉树就会退化成为单链,这时查找的时间复杂度也退化成O(n)的了。为了解决这个问题,我们希望二叉树是尽量平衡的,也就是说对于树中的任意一个结点,都能使其左子树上结点的个数和右子树上结点的个数相差不太多。

    定义

    平衡二叉树也叫AVL树。AVL树得名于它的发明者G.M.Adelson-Velsky和E.M.Landis,他们在1962年的论文"Analgorithm for the organization of information"中发表了它。

    平衡二叉树是具有以下性质的二叉查找树:对于树中的任意一个结点,都有该结点的左子树的高度与右子树的高度之差的绝对值小于2。与普通的BST相比,AVL树只是多定义了旋转操作,使得当左右子树的高度差的绝对值大于或者等于2时,平衡树可以自动地进行树形调整,以重新满足上述性质。

    名词

    平衡因子:每个结点的平衡因子就是该结点的左子树的高度减去右子树的高度,平衡二叉树的每个结点的平衡因子的绝对值不会超过2(或者大于1)。

    例子

    记住两张图一口诀即可。
    口诀:左子作父,父为右子,右孙变左孙

    右旋示意图


    image

    考虑顺序向二叉查找树中插入数据5,3,6,2,4,1,所得到的二叉查找树的形状如图 2.12(a) 所示。对于数据为 5 的这个结点,它的左子树高度与右子树高度之差为 2,我们可以通过改变5号结点和3号结点的相关指针将其变为如图 2.12(b) 所示的形状。由于3号结点的右孩子指针指向了5号结点,4号结点暂时脱离了二叉树,但注意到5号结点的左孩子指针变为空,我们可以将4号结点挂在5号结点的左孩子上,得到如图 2.12(c) 。这样,整棵树还继续满足平衡二叉树的性质。这个操作就像把树像旋纽一样向右旋转了下,我们把这个操作形象地称为右旋。右旋的口诀可以简单总结为“左子作父,父为右子,右孙变左孙”。由此可见,右旋操作的效果是原来的右孩子的深度(注意不是高度)加1,左孙的深度减1,而右孙的深度不变。而左旋的情况与右旋互为镜像,不再赘述。

    如果插入的顺序变成了5,2,6,1,3,4,所得到的二叉查找树的形状则如图 2.13(a) 所示。此时,5号结点的左子树与右子树的高度差仍然为2,这时再执行右旋行不行呢?我们可以尝试着做一下,右旋的结果如图 2.13(b) 所示,这时,新的树根2号结点的平衡因子为-2,显然直接右旋并没有使树重新平衡。这是因为右旋时右孙变左孙以后,深度没有变化,而显然造成树不平衡的主要原因在于右孙3号结点的深度过大。为了解决这个问题,尝试把它转化为第一个例子,就是对2号结点进行一次左旋,得到图 2.13(c) 。这样会使得右孙的深度转移到左孙。现在树的形状与上例相同了,再对这棵树进行右旋操作,会使得左孙深度减1,右孙不变。得到新的树是满足平衡二叉树的性质的,如图 2.13(d) 。


    image

    通过上面的两个例子,我们可以找出规律了。当向一棵平衡二叉树中插入一个新的结点时,有可能会使得某个结点的子树高度发生变化,从而影响了这个结点的平衡因子。当该结点的平衡因子为2时,就应该考虑对该点执行右旋操作。在执行右旋之前,还要检查一下左子树上的左孙和右孙的高度,如果是左孙的高度比较大,那么直接右旋就可以重新平衡,如果是右孙的高度比较大,那么就要在左孩子结点上先执行一次左旋。

    具体代码

    整体思路:

    1. 构建树的节点
    2. 插入方法
    3. 计算平衡因子
    4. 右旋
    5. 左旋
    6. 计算节点深度

    树节点

    组成部分:

    1. 父亲
    2. 左子
    3. 右子
    4. 数据域
    5. 平衡因子
    6. 当前树的深度
    public class AVLNode {
        public int data;
        public int depth;
        public int balance;
        public AVLNode parent;
        public AVLNode left;
        public AVLNode right;
    
        public AVLNode(int data) {
            this.data = data;
            this.depth = 1;
            this.balance = 0;
            this.left = null;
            this.right = null;
        }
    }
    

    插入方法

    普通的插入操作,同二叉树没啥区别

    public void insert(AVLNode root, int data) {
            if (data < root.data) {
                if (root.left != null) {
                    insert(root.left, data);
                } else {
                    //左子出生了
                    root.left = new AVLNode(data);
                    //把爹指定了
                    root.left.parent = root;
                }
            } else {
                if (root.right != null) {
                    insert(root.right, data);
                } else {
                    root.right = new AVLNode(data);
                    root.right.parent = root;
                }
            }
        }
    

    计算平衡因子

    然后要先获取平衡因子,计算方法前面已经提到过

    该结点的左子树的高度减去右子树的高度

        private int calcBalance(AVLNode p) {
            int left_depth;
            int right_depth;
    
            if (p.left != null) {
                left_depth = p.left.depth;
            } else {
                left_depth = 0;
            }
    
            if (p.right != null) {
                right_depth = p.right.depth;
            } else {
                right_depth = 0;
            }
    
            return left_depth - right_depth;
        }
    

    计算节点深度

    计算节点深度,初始深度都为1

        public int calcDepth(AVLNode p) {
            int depth = 0;
    
            if (p.left != null) {
                depth = p.left.depth;
            }
    
            if (p.right != null && depth < p.right.depth) {
                depth = p.right.depth;
            }
    
            depth++;
            return depth;
        }
    

    插入流程

    当该结点的平衡因子为2时,就应该考虑对该点执行右旋操作。在执行右旋之前,还要检查一下左子树上的左孙和右孙的高度,如果是左孙的高度比较大,那么直接右旋就可以重新平衡,如果是右孙的高度比较大,那么就要在左孩子结点上先执行一次左旋。

    //插入新节点之后,先计算树的平衡因子
            root.balance = calcBalance(root);
    
            //左子树高,右旋
            if (root.balance >= 2) {
                //右孙高,左旋
                if (root.left.balance == -1) {
                    left_rotate(root.left);
                }
                right_rotate(root);
            }
    
            if (root.balance <= -2) {
                if (root.right.balance == -1) {
                    right_rotate(root.right);
                }
                left_rotata(root);
            }
    
            root.balance = calcBalance(root);
            root.depth = calcDepth(root);
    

    右旋

    口诀

    左子作父,父为右子,(左子)右孙变(右子)左孙

    一次操作包括爷爷、父亲和孙子
    爷爷:pParent = p.parent
    左子:pLeftSon = p.left
    (左子的)右孙:pRightGrandSon = pLeftSon.right

    具体写法可以参考一下子步骤(参考上一页的图1)

    1. 将节点3变为爷爷
    2. 将节点5变为节点3的右子
    3. 将节点5的爸爸变为节点3
    4. 将节点5的左子变为节点4
    5. 将节点4的爸爸变为节点5
    private void right_rotate(AVLNode p) {
            //爷爷
            AVLNode pParent = p.parent;
            //左子
            AVLNode pLeftSon = p.left;
            //左子的右孙
            AVLNode pRightGrandSon = pLeftSon.right;
    
            // 节点3为爷爷
            pLeftSon.parent = pParent;
    
            // 节点3的右子为节点5 (左子为父的过程)
            pLeftSon.right = p;
    
            if (pParent != null) {
                if (p == pParent.left) {
                    pParent.left = pLeftSon;
                } else if (p == pParent.right) {
                    pParent.right = pLeftSon;
                }
            }
    
            // 节点5的爸爸为节点3
            p.parent = pLeftSon;
    
            // 节点5的左子变为节点4
            p.left = pRightGrandSon;
    
            // 考虑节点4为空的情况,不为空就把节点4的爸爸变为节点5
            if (pRightGrandSon != null) {
                pRightGrandSon.parent = p;
            }
            p.depth = calcDepth(p);
            p.balance = calcBalance(p);
            pLeftSon.depth = calcDepth(pLeftSon);
            pLeftSon.balance = calcBalance(pLeftSon);
        }
    

    左旋

    参考右旋写法即可

    private void left_rotate(AVLNode p) {
    
            AVLNode pParent = p.parent;
    
            AVLNode pRightSon = p.right;
    
            AVLNode pLeftGrandSon = pRightSon.left;
    
            pRightSon.parent = pParent;
    
            pRightSon.left = p;
    
            if (pParent != null) {
                if (p == pParent.right) {
                    pParent.right = pRightSon;
                } else if (p == pParent.left) {
                    pParent.left = pRightSon;
                }
            }else {
                root = pRightSon;
            }
    
            p.parent = pRightSon;
    
            p.right = pLeftGrandSon;
    
            if (pLeftGrandSon != null) {
                pLeftGrandSon.parent = p;
            }
            p.depth = calcDepth(p);
            p.balance = calcBalance(p);
            pRightSon.depth = calcDepth(pRightSon);
            pRightSon.balance = calcBalance(pRightSon);
        }
    

    按层遍历

    再写一个按层遍历二叉树的方法,为了简单,这里直接用递归法

    public List<List<Integer>> levelOrder() {
            List<List<Integer>> result = new ArrayList();
            helper(result, root, 0);
            return result;
        }
    
        private void helper(List<List<Integer>> result, AVLNode node, int level) {
            if (node == null) {
                return;
            }
    
            if (result.size() == level) {
                result.add(new ArrayList());
            }
    
            result.get(level).add(node.data);
            helper(result, node.left, level + 1);
            helper(result, node.right, level + 1);
        }
    

    为了方便测试,调整了整体的代码,现将完整代码列出:

    import java.util.ArrayList;
    import java.util.LinkedList;
    import java.util.List;
    
    public class MyAVLTree {
    
        private AVLNode root;
    
        public void insert(int data) {
            if (root == null) {
                root = new AVLNode(data);
            } else {
                insert(root, data);
            }
        }
    
        private void insert(AVLNode root, int data) {
            if (data < root.data) {
                if (root.left != null) {
                    insert(root.left, data);
                } else {
                    //左子出生了
                    root.left = new AVLNode(data);
                    //把爹指定了
                    root.left.parent = root;
                }
            } else {
                if (root.right != null) {
                    insert(root.right, data);
                } else {
                    root.right = new AVLNode(data);
                    root.right.parent = root;
                }
            }
    
            //插入新节点之后,先计算树的平衡因子
            root.balance = calcBalance(root);
    
            //左子树高,右旋
            if (root.balance >= 2) {
                //右孙高,左旋
                if (root.left.balance == -1) {
                    left_rotate(root.left);
                }
                right_rotate(root);
            }
    
            if (root.balance <= -2) {
                if (root.right.balance == -1) {
                    right_rotate(root.right);
                }
                left_rotate(root);
            }
    
            root.balance = calcBalance(root);
            root.depth = calcDepth(root);
    
        }
    
        private void left_rotate(AVLNode p) {
    
            AVLNode pParent = p.parent;
    
            AVLNode pRightSon = p.right;
    
            AVLNode pLeftGrandSon = pRightSon.left;
    
            pRightSon.parent = pParent;
    
            pRightSon.left = p;
    
            if (pParent != null) {
                if (p == pParent.right) {
                    pParent.right = pRightSon;
                } else if (p == pParent.left) {
                    pParent.left = pRightSon;
                }
            }else {
                root = pRightSon;
            }
    
            p.parent = pRightSon;
    
            p.right = pLeftGrandSon;
    
            if (pLeftGrandSon != null) {
                pLeftGrandSon.parent = p;
            }
            p.depth = calcDepth(p);
            p.balance = calcBalance(p);
            pRightSon.depth = calcDepth(pRightSon);
            pRightSon.balance = calcBalance(pRightSon);
        }
    
        private void right_rotate(AVLNode p) {
            //爷爷
            AVLNode pParent = p.parent;
            //左子
            AVLNode pLeftSon = p.left;
            //左子的右孙
            AVLNode pRightGrandSon = pLeftSon.right;
    
            // 节点3为爷爷
            pLeftSon.parent = pParent;
    
            // 节点3的右子为节点5 (左子为父的过程)
            pLeftSon.right = p;
            if (pParent != null) {
                if (p == pParent.left) {
                    pParent.left = pLeftSon;
                } else if (p == pParent.right) {
                    pParent.right = pLeftSon;
                }
            }else {
                root = pLeftSon;
            }
    
            // 节点5的爸爸为节点3
            p.parent = pLeftSon;
    
            // 节点5的左子变为节点4
            p.left = pRightGrandSon;
    
            // 考虑节点4为空的情况,不为空就把节点4的爸爸变为节点5
            if (pRightGrandSon != null) {
                pRightGrandSon.parent = p;
            }
            p.depth = calcDepth(p);
            p.balance = calcBalance(p);
            pLeftSon.depth = calcDepth(pLeftSon);
            pLeftSon.balance = calcBalance(pLeftSon);
        }
    
        private int calcBalance(AVLNode p) {
            int left_depth;
            int right_depth;
    
            if (p.left != null) {
                left_depth = p.left.depth;
            } else {
                left_depth = 0;
            }
    
            if (p.right != null) {
                right_depth = p.right.depth;
            } else {
                right_depth = 0;
            }
    
            return left_depth - right_depth;
        }
    
        public int calcDepth(AVLNode p) {
            int depth = 0;
    
            if (p.left != null) {
                depth = p.left.depth;
            }
    
            if (p.right != null && depth < p.right.depth) {
                depth = p.right.depth;
            }
    
            depth++;
            return depth;
        }
    
        public List<List<Integer>> levelOrder(){
            List<List<Integer>> result = new ArrayList();
            helper(result, root, 0);
            return result;
        }
    
        public void helper(List<List<Integer>> result, AVLNode node, int level){
            if(node == null){
                return;
            }
    
            if(result.size() == level){
                result.add(new ArrayList());
            }
    
            result.get(level).add(node.data);
            helper(result, node.left, level+1);
            helper(result, node.right, level+1);
        }
    
        class AVLNode {
            public int data;
            public int depth;
            public int balance;
            public AVLNode parent;
            public AVLNode left;
            public AVLNode right;
    
            public AVLNode(int data) {
                this.data = data;
                this.depth = 1;
                this.balance = 0;
                this.left = null;
                this.right = null;
            }
        }
    }
    
    

    测试程序:

    public class TestMyAVLTree {
    
        public static void main(String[] args){
    
            MyAVLTree myAVLTree = new MyAVLTree();
            myAVLTree.insert(5);
            myAVLTree.insert(2);
            myAVLTree.insert(6);
            myAVLTree.insert(1);
            myAVLTree.insert(3);
            myAVLTree.insert(4);
    
    //        myAVLTree.insert(5);
    //        myAVLTree.insert(3);
    //        myAVLTree.insert(6);
    //        myAVLTree.insert(2);
    //        myAVLTree.insert(4);
    //        myAVLTree.insert(1);
    
            System.out.println(myAVLTree.levelOrder());
    
        }
    
    }
    
    

    相关文章

      网友评论

          本文标题:自己动手写数据结构之平衡二叉树

          本文链接:https://www.haomeiwen.com/subject/dcbyoktx.html