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

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

作者: 逍遥白亦 | 来源:发表于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