[数据结构与算法] 树结构之二叉排序树、平衡二叉树、多路查找树

时间:2022-07-22
本文章向大家介绍[数据结构与算法] 树结构之二叉排序树、平衡二叉树、多路查找树,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

BST

介绍

需求: 给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加

解决方案——使用数组

  • 数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢.
  • 数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。
  • 使用链式存储-链表. 不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。

在此基础上,我们引入了二叉排序树, 它能够高效的实现数组的添加和删除

二叉排序树介绍

二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。

  • 特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
  • 比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树以及添加一个元素2后二叉排序树为:

创建和遍历

  • 创建节点类, 定义相关属性构造方法以及ToSting()方法
  • 编写BST的创建方法, 判断传入的节点是否为空 传入的节点的值小于当前节点, 插入到左子树(判断非空), 左子树递归调用创建节点的方法 传入的节点的值大于当前节点, 插入到右子树(判断非空), 右子树递归调用创建节点的方法
  • 编写中序遍历方法
  • 创建二叉排序树类,定义根节点. 根据根节点是否为空的情况, 编写二叉排序树的创建和遍历方法
  • 测试代码
public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 13, 5, 1, 9};
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环将节点添加到二叉树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node(arr[i]));
        }

        //中序遍历二叉树
        System.out.println("中序遍历二叉树");
        binarySortTree.midOrder();
    }
}

/**
 * 创建二叉排序树
 */
class BinarySortTree{
    //创建根节点
    private Node root;

    //创建二叉排序树的添加方法
    public void add(Node node){
        if (root==null){
            root = node;
        }else {
            root.add(node);
        }

    }


    //创建二叉排序树的中序遍历方法
    public void midOrder(){
        if (root==null){
            System.out.println("该树为空,无法进行中序遍历");
        }else {
            root.midOder();
        }

    }

}

/**
 * 1.创建节点
 */
class Node{
    int value;
    Node left;
    Node right;

    public Node(int value){
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //二叉排序树的生成
    public void add(Node node){
        if (node==null){//如果为空,直接返回
            return;
        }
        //传入的节点的值小于当前节点
        if (node.value<this.value){//插入到左子树
            if (this.left==null){
                this.left = node;
            }else {
                //递归调用添加的方法
                this.left.add(node);
            }
        }else {
            //传入节点的值大于当前节点
            if (this.right==null){
                this.right = node;
            }else {
                //右子树递归调用添加的方法
                this.right.add(node);
            }
        }
    }

    public void midOder(){
        if (this.left!=null){
            this.left.midOder();
        }
        System.out.println(this);
        if (this.right!=null){
            this.right.midOder();
        }
    }


}

测试结果

删除

二叉排序树的删除情况比较复杂,有下面三种情况需要考虑

  • 删除叶子节点 (比如:2, 5, 9, 12)
  • 删除只有一颗子树的节点 (比如:1)=>删除的节点的值=子树节点
  • 删除有两颗子树的节点. (比如:7, 3,10 )=>删除的节点的值=任意一边的最小值

实现思路

public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环将节点添加到二叉树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node(arr[i]));
        }

        //中序遍历二叉树
        System.out.println("中序遍历二叉树");
        binarySortTree.midOrder();

        //测试删除叶子节点
        binarySortTree.delNode(2);
        binarySortTree.delNode(5);
        binarySortTree.delNode(9);
        binarySortTree.delNode(12);
        //测试删除含有两个叶子节点的节点
        binarySortTree.delNode(7);
        binarySortTree.delNode(3);//原来3的位置放的是2
        binarySortTree.delNode(10);//原来10的位置放的是9
        binarySortTree.delNode(1);//原来10的位置放的是9
        System.out.println("执行删除操作完毕");
        binarySortTree.midOrder();
    }
}

/**
 * 创建二叉排序树
 */
class BinarySortTree{
    //创建根节点
    private Node root;

    //创建二叉排序树的添加方法
    public void add(Node node){
        if (root==null){
            root = node;
        }else {
            root.add(node);
        }

    }


    //创建二叉排序树的中序遍历方法
    public void midOrder(){
        if (root==null){
            System.out.println("该树为空,无法进行中序遍历");
        }else {
            root.midOder();
        }

    }

    /**
     * 查找要删除的节点
     * @param value
     * @return 如果有返回该节点,,如果没有返回null
     */
    public Node search(int value){
        if (root==null){
            return null;
        }else {
            return root.search(value);
        }
    }

    /**
     * 查找要删除的父节点
     * @param value
     * @return 如果有返回该节点,如果没有返回null
     */
    public Node searchParent(int value){
        if (root==null){
            return null;
        }else {
            return root.searchParent(value);
        }
    }

    /**
     * 删除对应的节点
     * @param value
     */
    public void delNode(int value){
        if (root==null){
            return;
        }else {
            //如果根节点不会空则进行节点的删除操作
            //1. 查找要删除的节点targetNode
            Node targetNode = search(value);
            //如果没有找到要删除的节点
            if (targetNode==null){
                return;
            }
            //如果要查找的二叉树只有一个节点
            if (root.left==null && root.right==null){
                root = null;
                return;
            }
            //2.去找到targetNode的父节点
            Node parent = searchParent(value);
            //a.如果要删除的节点都是叶子节点
            if (targetNode.left==null && targetNode.right==null){
                if (parent.left!=null && parent.left.value==value){//是左子节点
                    parent.left = null;
                }else if(parent.right!=null && parent.right.value==value){//是右子节点
                    parent.right = null;
                }
            }else if(targetNode.left!=null && targetNode.right!=null){//b.删除有两个子树的节点
                //在右树中找最小的
                int minVal = delRightTreeMin(targetNode.right);
                targetNode.value = minVal;
                System.out.println("minVal = " + minVal);
            }else {//c.删除只有一个子树的节点
                //x如果要删除的节点有左子节点
                if (targetNode.left!=null){
                   if (parent!=null){//***防止出现在删除最后两个节点时,首先删除根节点的情况
                       if (parent.left.value==value){//如果targetNode是parent的左子节点
                           parent.left = targetNode.left;
                       }else {//如果targetNode是parent的右子节点
                           parent.right = targetNode.left;
                       }
                   }else {
                       root = targetNode.left;
                   }
                }else {//x如果要删除的节点有右子节点
                    if (parent!=null){//***防止出现在删除最后两个节点时,首先删除根节点的情况
                        if (parent.left.value==value){//如果targetNode是parent的左子节点
                            parent.left = targetNode.right;
                        }else {//如果targetNode是parent的右子节点
                            parent.right = targetNode.right;
                        }
                    }else {
                        root = targetNode.right;
                    }

                }

            }

        }

    }



    /**
     * 编写方法:
     * 1.返回以node为根节点的二叉树的最小节点的值
     * 2.删除以node为根节点的二叉树的最小节点
     * @param node
     * @return
     */
    public int delRightTreeMin(Node node){
        Node target = node;
        // 循环的查找左子节点,就会找到最小值
        while (target.left!=null){
            target = target.left;
        }
        // 如果左子节点为空,说明找到了最小节点
        // 删除最小节点
        delNode(target.value);
        return target.value;
    }
}

/**
 * 1.创建节点
 */
class Node{
    int value;
    Node left;
    Node right;

    public Node(int value){
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //二叉排序树的生成
    public void add(Node node){
        if (node==null){//如果为空,直接返回
            return;
        }
        //传入的节点的值小于当前节点
        if (node.value<this.value){//插入到左子树
            if (this.left==null){
                this.left = node;
            }else {
                //递归调用添加的方法
                this.left.add(node);
            }
        }else {
            //传入节点的值大于当前节点
            if (this.right==null){
                this.right = node;
            }else {
                //右子树递归调用添加的方法
                this.right.add(node);
            }
        }
    }

    public void midOder(){
        if (this.left!=null){
            this.left.midOder();
        }
        System.out.println(this);
        if (this.right!=null){
            this.right.midOder();
        }
    }

    /**
     * 查找要删除的节点
     * @param value 要删除的节点的值
     * @return 如果找到返回该节点,否则返回null
     */
    public Node search(int value){
        if (value<this.value){//如果小于父节点,则进入左子树
            if (this.left==null){
                return null;
            }
            return this.left.search(value);//左子树递归调用查找的方法
        }else if(value>this.value){//如果大于父节点,则进行入右子树
            if (this.right==null){
                return null;
            }
            return this.right.search(value);//右子树递归调用查找方法
        }else {//如果查找的值等于父节点,则直接返回该节点
            return this;
        }
    }

    /**
     * 查找要删除的父节点
     * @param value 要删除的节点的值
     * @return 要删除的父节点, 否则返回null
     */
    public Node searchParent(int value){
        //如果当前节点就是要删除的父节点, 则直接返回
        if ( (this.left!=null && this.left.value==value) || (this.right!=null && this.right.value==value) ){
            return this;
        }else {
            // 如果需要找的值小于当前节点则递归调用左子树(判空)
            if (this.left!=null && value<this.value){
                return this.left.searchParent(value);
                // 如果需要找到值大于当前节点则递归调用右子树(判空)
            }else if (this.right!=null && value>this.value){
                return this.right.searchParent(value);
            }else {
                return null;//没有找到父节点
            }
        }

    }

}

测试结果

AVL树

看一个案例(说明二叉排序树可能的问题) 给你一个数列{1,2,3,4,5,6},要求创建一个二叉排序树(BST), 并分析问题所在

介绍

平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。 具有以下特点:

  • 它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
  • 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。

举例: 下图1,2是AVL树,3不是. 因为左右两个子树高度差为2

AVL树的左旋转

AVL左旋转思路图解

  // 返回左子树的高度
    public int leftHeight() {
        if (left == null) {
            return 0;
        }
        return left.height();
    }

    // 返回右子树的高度
    public int rightHeight() {
        if (right == null) {
            return 0;
        }
        return right.height();
    }

    // 返回以该结点为根结点的树的高度
    public int height() {
        return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
    }

    /**
     * 相当于上面使用三目运算符实现的返回根节点的树的高度的方法
     * @return
     */
  /*  public int height(){
        int leftVal = 0;
        int rightVal = 0;
        //如果左树不为空,则递归调用该方法
        if (left!=null){
            leftVal=left.height();
        }
        //如果右树不为空,则递归调用该方法
        if (right!=null){
            rightVal = right.height();
        }
        // 返回时,需要将本层的高度加上
        return Math.max(leftVal, rightVal)+1;
    }*/

   //左旋转方法
    private void leftRotate() {

        //创建新的结点,以当前根结点的值
        Node newNode = new Node(value);
        //把新的结点的左子树设置成当前结点的左子树
        newNode.left = left;
        //把新的结点的右子树设置成当前结点的右子树的左子树
        newNode.right = right.left;
        //把当前结点的值替换成右子结点的值
        value = right.value;
        //把当前结点的右子树设置成当前结点右子树的右子树
        right = right.right;
        //把当前结点的左子树(左子结点)设置成新的结点
        left = newNode;


    }

AVL树的右旋转

AVl右旋转思路图解

//右旋转方法
    private void rightRotate() {
        Node newNode = new Node(value);
        newNode.right = right;
        newNode.left = left.right;
        value = left.value;
        left = left.left;
        right = newNode;
    }

AVL树的双旋转

AVL树的双旋转的图解

多路查找树

二叉树的操作效率较高,但是也存在问题, 请看下面的二叉树

二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿), 就存在如下问题:

  • 问题1:在构建二叉树时,需要多次进行i/o操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响
  • 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度.

如果对二叉查找树/二叉排序树和平衡二叉树等概念理解不清楚, 建议看看下面 理解完全二叉树、平衡二叉树、二叉查找树

多叉树

在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)

后面我们讲解的2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。

举例说明(下面2-3树就是一颗多叉树)

B树

B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。

如下图B树通过重新组织节点, 降低了树的高度.

  • 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次I/O就可以完全载入
  • 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素, B树(B+)广泛应用于文件存储系统以及数据库系统中

2-3树

2-3树是最简单的B树结构, 具有如下特点:

  • 2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
  • 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
  • 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点.
  • 2-3树是由二节点和三节点构成的树

插入规则:

  • 2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
  • 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
  • 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
  • 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满- 足上面3个条件。
  • 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则

下图是模拟2-3树创建的结构图

除了23树,还有234树等,概念和23树类似,也是一种B树。它除了每个节点所容纳的元素个数不同外, 创建方式和23树一样 如图

B树的介绍

前面已经介绍了2-3树和2-3-4树,他们就是B树(英语:B-tree 也写成B-树),这里我们再做一个说明,我们在学习Mysql时,经常听到说某种类型的索引是基于B树或者B+树的,如下图:

树的说明:

  • B树的阶:节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4
  • B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
  • 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据.
  • 搜索有可能在非叶子结点结束
  • 搜索性能等价于在关键字全集内做一次二分查找

B+树

B+树是B树的变体,也是一种多路搜索树。

B+树的说明:

  • B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
  • 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。
  • 不可能在非叶子结点命中 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
  • 更适合文件索引系统
  • B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然.

B* 树

B* 树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。(和B树,B+树区别在此)

B*树的说明:

  • B* 树定义了非叶子结点关键字个数至少为(2/3)*M(M为树的度),即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2。
  • 从第1个特点我们可以看出,B*树分配新结点的概率比B+树要低,空间使用率更高