数据结构(七)---二叉树

一、树的基本介绍

树分成两部分:基础部分+应用部分

(一)基础部分

1)数组存储方式的分析
有点:可以通过下标访问元素,速度快,对于有序数组,还可以使用二分查找提高检索速度
缺点:如果要检索某个具体的值,或者插入值,会导致整体移动,效率很低

2)链式存储方式的分析
优点:插入删除节点时,不会导致整体移动,效率比数组要好
缺点:再检索时,不如有索引的数组块,需要从头开始遍历

3)树存储方式的分析
优点:能提高数据的存储、读取的效率,比如利用二叉排序树,既可以保证数据的检索速度,同时保证数据的插入、删除、修改的速度

如果用“二叉排序树”来存储数据,那么对数据的增删改查的效率都可以提高

4)完全二叉树
若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树

5)满二叉树
除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。

堆:堆是具有以下性质的完全二叉树,每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

6)平衡二叉树
平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等

(二)二叉树遍历

1)创建一颗二叉树
2)前序遍历
2.1)先输出当前节点(初始的时候是root节点)
2.2)如果左子节点不为空,则递归继续前序遍历
2.3)如果右子节点不为空,则递归继续前序遍历

3)中序遍历
3.1)如果左子节点不为空,则递归继续前序遍历
3.2)输出当前节点
3.3)如果右子节点不为空,则递归继续前序遍历

4)后序遍历
4.1)如果左子节点不为空,则递归继续前序遍历
4.2)如果右子节点不为空,则递归继续前序遍历
4.3)输出当前节点

(三)使用前序、中序、后序的方式来查询指定的结点

前序查找思路:
1)先判断当前节点的no是否是要查找的
2)如果是相等,则返回当前结点
3)如果不相等,则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
4)如果左递归前序查找,找到节点,则返回,否则继续判断,当前的节点的右子节点是否为空,
如果不空,则继续向右递归前序查找

中序查找思路:
1)先判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
2)如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点,否则继续进行右递归的中序查找
3)如果右递归中序查找,找到就返回,找到就返回,否则返回null

后序查找思路:
1)判断当前结点的左子节点是否为空,如果不为空,则递归后续查找
2)如果找到,就返回,如果没有找到,就判断当前结点的右子节点是否为空,如果不为空,则右递归进行后序查找,如果找到,就返回
3)就和当前结点进行,比如,如果是则返回,否则返回null

(四)删除二叉树的结点

规定:
1)如果删除的结点是叶子结点,则删除该节点
2)如果删除的节点是非叶子界定啊,则删除该子树

思路:(判断的不是当前节点,而是当前节点的左右子节点)
首先处理:考虑如果树是空树root,如果只有一个root节点,则等价把二叉树置空
1)因为我们的二叉树是单向的,所以我们是判断当前节点的子节点是否是待删除的节点
而不能去判断当前节点是不是待删除的节点
2)如果当前节点的左子节点不为空,并且左子节点就是待删除的节点,就把this.left=null,把左子树置为空
3)如果当前节点的右子节点不为空,并且右子节点就是待删除的节点,就把this.right=null,把右子树置为空
4)如果2,3两步都没有删除节点,那就需要向左子树进行递归删除
5)如果4步向左子树递归删除也没有删除成功,那就需要向右子树进行递归删除

(五)顺序存储二叉树(堆排序就会用到顺序存储二叉树)

基本说明:
从数据存储来看,数组存储方式和树的存储方式可以相互转换,就是说数组可以转换成树,树叶可以转换成数组

要求:
1)二叉树的节点,要求以数组的方式来存放arr
2)要求在遍历数组arr时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成节点的遍历

需要熟悉二叉树的特点:
1)顺序二叉树通常只考虑完全二叉树
2)第n个元素的左子节点为2n+1
3)第n个元素的右子节点为2
n+2
4)第n个元素的父节点为(n-1)/2
5)n:表示二叉树中的第几个元素(按0开始编号)

(六)线索化二叉树

问题分析:
1)当我们对上面的二叉树进行中序遍历时
2)但是好几个节点的左右指针,并没有完全的利用上
3)如果我们希望充分的利用各个节点的左右指针,让各个节点可以指向自己的前后节点怎么办?
4)解决方法就是使用——线索二叉树

线索二叉树的介绍:
1)n个节点的二叉链表中含有n+1个空指针域。如果在这些空指针域中存放指向节点的前驱和后继节点的指针,就可以充分利用
2)二叉链表加上线索(前驱后继指针)==》线索链表,二叉树加上线索就成了线索二叉树
分成:1-前序线索二叉树,2-中序线索二叉树,3-后序线索二叉树
3)一个节点的前一个节点,叫前驱节点
4)一个节点的后一个节点,叫后继节点

思路分析:
比如说中序线索二叉树,先写出来二叉树的中序遍历{8,3,10,1,14,6}
然后根据中序遍历,在二叉树里找到前后关系,画上线索表示前驱和后继

特点说明:
1)left指向的是左子树,也可能是指向的前驱节点
2)right指向的右子树,也可能是指向后继节点

(七)遍历线索化二叉树

说明:对中序线索化的二叉树进行遍历
分析:线索化后,各个节点指向有变化,因此原来的遍历方式不能再用了
这时候要用新的的方式遍历线索化二叉树,各个节点可以通过线型方式遍历
因此,不需要使用递归方式,也就提高了遍历的效率,遍历的次序应当和中序遍历保持一致

(八)堆排序的基本思想

1)把待排序序列构造成一个大顶堆
2)此时,整个序列的最大值就是堆顶的根节点
3)把其与末尾元素进行交换,此时末尾就为最大值
4)然后把剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,就能得到一个有序序列了

举例:{4,6,8,5,9}
1)首先,有一个无序序列结构,在这个基础上进行排序
2)此时我们从最后一个非叶子节点开始(叶子节点不需要调整),从左至右,从上至下进行调整
第一个非叶子节点怎么得来的呢——arr.length/2-1=5/2-1=1
3)编号为1的非叶子节点,假设为6,把6和6的左右子节点(也就是叶子节点)进行比较,
如果6比左右子节点都大,就不需要调整
如果6比右子节点小,那就应该把6和右子节点互换位置
如果6比左右子节点都小,那就应该和最大的那个进行交换
4)接着找第二个非叶子节点,继续进行交换
交换过后,会出现一种情况,新的节点位置可能会影响下面子树的结构
这时候,继续调整
5)到最后,最大的值会移动到根节点的位置
把根节点和树末尾的值交换位置,然后取出这个最大值

二、前中后遍历二叉树

创建节点类

//先创建节点
class HeroNode {
    private int no;
    private String name;
    private HeroNode left;
    private HeroNode right;

    //构造器
    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    //编写递归删除的方法
    //如果删除的节点是叶子节点,就删除该节点
    //如果删除的节点是非叶子节点,就删除该子树
    public void delNode(int no) {
        // 思路:(判断的不是当前节点,而是当前节点的左右子节点)
        // 首先处理:考虑如果树是空树root,如果只有一个root节点,则等价把二叉树置空
        // 1)因为我们的二叉树是单向的,所以我们是判断当前节点的子节点是否是待删除的节点
        //         而不能去判断当前节点是不是待删除的节点
        // 2)如果当前节点的左子节点不为空,并且左子节点就是待删除的节点,就把this.left=null,把左子树置为空
        if (this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        // 3)如果当前节点的右子节点不为空,并且右子节点就是待删除的节点,就把this.right=null,把右子树置为空
        if (this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        // 4)如果2,3两步都没有删除节点,那就需要向左子树进行递归删除
        if (this.left != null) {
            this.left.delNode(no);
        }
        // 5)如果4步向左子树递归删除也没有删除成功,那就需要向右子树进行递归删除
        if (this.right != null) {
            this.right.delNode(no);
        }
    }

    //编写前序遍历的方法
    public void preOrder() {
        //先输出当前节点
        System.out.println(this);
        //再输出左子节点(递归)——递归向左子树前序遍历
        if (this.left != null) {
            this.left.preOrder();
        }
        //最后输出右子节点(递归)——递归向右子树前序遍历
        if (this.right != null) {
            this.right.preOrder();
        }
    }

    //编写中序遍历的方法
    public void infixOrder() {
        //输出左子节点(递归)——递归向左子树中序遍历
        if (this.left != null) {
            this.left.infixOrder();
        }
        //输出父节点
        System.out.println(this);
        //输出右子节点(递归)——递归向右子树中序遍历
        if (this.right != null) {
            this.right.infixOrder();
        }
    }

    //编写后序遍历的方法
    public void postOrder() {
        //输出左子节点(递归)——递归向左子树后序遍历
        if (this.left != null) {
            this.left.postOrder();
        }
        //输出右子节点(递归)——递归向右子树后序遍历
        if (this.right != null) {
            this.right.postOrder();
        }
        //输出父节点
        System.out.println(this);
    }

    /**
     * 前序遍历查找
     * 如果找到就返回该Node,如果没有找到返回null
     *
     * @MethodName: preOrderSearch
     * @Author: AllenSun
     * @Date: 2019/11/4 23:24
     */
    public HeroNode preOrderSearch(int no) {
        System.out.println("进入前序遍历");//这句话用来统计一共进行了几次前序遍历

        //比较当前结点是不是要找的节点
        if (this.no == no) {
            return this;
        }
        //1-则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
        //2-如果左递归前序查找,找到节点,则返回
        HeroNode resNode = null;
        if (this.left != null) {
            resNode = this.left.preOrderSearch(no);
        }
        if (resNode != null) {
            //说明我们左子树找到,得到节点,则返回,否则继续判断
            return resNode;
        }
        //如果左递归没有找到,就判断判断当前结点的右子节点是否为空,如果不为空,则继续向右递归前序查找
        if (this.right != null) {
            resNode = this.right.preOrderSearch(no);
        }
        return resNode;
    }

    /**
     * 中序遍历查找
     *
     * @MethodName:
     * @Author: AllenSun
     * @Date: 2019/11/4 23:41
     */
    public HeroNode infixOrderSearch(int no) {
        //判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
        HeroNode resNode = null;
        if (this.left != null) {
            resNode = this.left.infixOrderSearch(no);
        }
        if (resNode != null) {
            return resNode;
        }
        System.out.println("进入中序遍历");//这句话用来统计一共进行了几次中序遍历
        //如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
        if (this.no == no) {
            return this;
        }
        //否则继续进行右递归的中序查找
        if (this.right != null) {
            resNode = this.right.infixOrderSearch(no);
        }
        return resNode;
    }

    /**
     * 后序遍历
     *
     * @MethodName:
     * @Author: AllenSun
     * @Date: 2019/11/4 23:59
     */
    public HeroNode postOrderSearch(int no) {
        //判断当前结点左子节点是否为空,如果不为空,则递归后序查找
        HeroNode resNode = null;
        if (this.left != null) {
            resNode = this.left.postOrderSearch(no);
        }
        if (resNode != null) {//说明在左子树找到
            return resNode;
        }
        //如果左子树没有找到,那就到右子树去找递归查找
        if (this.right != null) {
            resNode = this.right.postOrderSearch(no);
        }
        if (resNode != null) {
            return resNode;
        }
        System.out.println("进入后序遍历");//这句话用来统计一共进行了几次后序遍历
        //如果左右子树都没有找到,那就比较当前的节点
        if (this.no == no) {
            return this;
        }
        return resNode;
    }

}

创建一个树类

//创建一个树
class BinaryTree {
    private HeroNode root;//根节点

    public void setRoot(HeroNode root) {
        this.root = root;
    }
    //删除节点
    public void delNode(int no){
        if(root!=null){
            //如果只有根节点,这里理解判断root是不是就是要删除节点
            if(root.getNo()==no){
                root=null;
            } else {
                //递归删除
                root.delNode(no);
            }
        } else {
            System.out.println("空树,不能删除");
        }
    }

    //前序遍历
    public void preOrder() {
        if (this.root != null) {
            this.root.preOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    //中序遍历
    public void infixOrder() {
        if (this.root != null) {
            this.root.infixOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    //后序遍历
    public void postOrder() {
        if (this.root != null) {
            this.root.postOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    //前序遍历查找
    public HeroNode preOrderSearch(int no) {
        if (root != null) {
            return root.preOrderSearch(no);
        } else {
            return null;
        }
    }

    //中序遍历查找
    public HeroNode infixOrderSearch(int no) {
        if (root != null) {
            return root.infixOrderSearch(no);
        } else {
            return null;
        }
    }

    //后序遍历查找
    public HeroNode postOrderSearch(int no) {
        if (root != null) {
            return root.postOrderSearch(no);
        } else {
            return null;
        }
    }
}

测试类

public class BinaryTreeDemo {
    public static void main(String[] args) {
        //先需要创建一颗二叉树
        BinaryTree binaryTree = new BinaryTree();
        //创需要的节点
        HeroNode root = new HeroNode(1, "宋江");
        HeroNode node2 = new HeroNode(2, "吴用");
        HeroNode node3 = new HeroNode(3, "卢俊义");
        HeroNode node4 = new HeroNode(4, "林冲");
        HeroNode node5 = new HeroNode(5, "关胜");

        //说明,我们先手动创建二叉树,后面我们学习递归的方式创建二叉树
        binaryTree.setRoot(root);//设置根节点
        root.setLeft(node2);//设置左子树
        root.setRight(node3);
        node3.setRight(node4);
        node3.setLeft(node5);

        //测试一下前中后遍历
        System.out.println("前序遍历:");
        binaryTree.preOrder();
        System.out.println("中序遍历:");
        binaryTree.infixOrder();
        System.out.println("后序遍历:");
        binaryTree.postOrder();

        //前序遍历查找
        //前序遍历的次数:4次
        System.out.println("前序遍历方式");
        HeroNode resNode01 = binaryTree.preOrderSearch(5);
        if (resNode01 != null) {
            System.out.printf("找到了,信息为no=%d  name=%s\n", resNode01.getNo(), resNode01.getName());
        } else {
            System.out.printf("没有找到信息为no=%d  name=%s 的英雄\n", resNode01.getNo(), resNode01.getName());
        }

        //中序遍历查找
        //中序遍历的次数:3次
        System.out.println("中序遍历方式");
        HeroNode resNode02 = binaryTree.infixOrderSearch(5);
        if (resNode02 != null) {
            System.out.printf("找到了,信息为no=%d  name=%s\n", resNode02.getNo(), resNode02.getName());
        } else {
            System.out.printf("没有找到信息为no=%d  name=%s 的英雄\n", resNode02.getNo(), resNode02.getName());
        }

        //后序遍历查找
        //后序遍历的次数:2次
        System.out.println("后序遍历方式");
        HeroNode resNode03 = binaryTree.postOrderSearch(5);
        if (resNode03 != null) {
            System.out.printf("找到了,信息为no=%d  name=%s\n", resNode03.getNo(), resNode03.getName());
        } else {
            System.out.printf("没有找到信息为no=%d  name=%s 的英雄\n", resNode03.getNo(), resNode03.getName());
        }

        //测试一把删除节点
        System.out.println("删除前,前序遍历");
        binaryTree.preOrder();
        binaryTree.delNode(5);
        System.out.println("删除后,前序遍历");
        binaryTree.preOrder();

    }
}

三、顺序存储二叉树

编写一个ArrayBinaryTree,实现顺序存储二叉树遍历

//编写一个ArrayBinaryTree,实现顺序存储二叉树遍历
class ArrBinaryTree {
    private int arr[];//存储数据节点的数组

    public ArrBinaryTree(int arr[]) {
        this.arr = arr;
    }

    //重载preOrder
    public void preOrder(){
        this.preOrder(0);
    }

    //编写一个方法,完成顺序存储二叉树的前序遍历
    public void preOrder(int index) {
        //如果数组为空,或者arr.length=0
        if (arr == null || arr.length == 0) {
            System.out.println("数组为空,不能按照二叉树的前序遍历");
        }
        System.out.print(arr[index]+"\t");//输出当前的元素

        //向左递归遍历
        if ((index * 2 + 1) < arr.length) {
            preOrder(2 * index + 1);
        }
        //向右递归遍历
        if ((index * 2 + 2) < arr.length) {
            preOrder(2 * index + 2);
        }
    }
}

测试类

public class ArrBinaryTreeDemo {
    public static void main(String[] args) {
        int arr[] = {1, 2, 3, 4, 5, 6, 7};
        //创建一个ArrBinaryTree
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
        arrBinaryTree.preOrder(0);//1,2,4,5,3,6,7
    }
}

四、线索二叉树

创建节点类

//创建节点HeroNode
class HeroNode {
    private int no;
    private String name;
    private HeroNode left;
    private HeroNode right;

    //线索二叉树说明
    //1-如果leftType==0 表示指向的是左子树,如果1则表示指向前驱节点
    //2-如果rightType==0 表示指向的是右子树,如果1则表示指向后继节点
    private int leftType;
    private int rightType;

    public int getLeftType() {
        return leftType;
    }

    public void setLeftType(int leftType) {
        this.leftType = leftType;
    }

    public int getRightType() {
        return rightType;
    }

    public void setRightType(int rightType) {
        this.rightType = rightType;
    }

    //构造器
    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    //编写递归删除的方法
    //如果删除的节点是叶子节点,就删除该节点
    //如果删除的节点是非叶子节点,就删除该子树
    public void delNode(int no) {
        // 思路:(判断的不是当前节点,而是当前节点的左右子节点)
        // 首先处理:考虑如果树是空树root,如果只有一个root节点,则等价把二叉树置空
        // 1)因为我们的二叉树是单向的,所以我们是判断当前节点的子节点是否是待删除的节点
        //         而不能去判断当前节点是不是待删除的节点
        // 2)如果当前节点的左子节点不为空,并且左子节点就是待删除的节点,就把this.left=null,把左子树置为空
        if (this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        // 3)如果当前节点的右子节点不为空,并且右子节点就是待删除的节点,就把this.right=null,把右子树置为空
        if (this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        // 4)如果2,3两步都没有删除节点,那就需要向左子树进行递归删除
        if (this.left != null) {
            this.left.delNode(no);
        }
        // 5)如果4步向左子树递归删除也没有删除成功,那就需要向右子树进行递归删除
        if (this.right != null) {
            this.right.delNode(no);
        }
    }

    //编写前序遍历的方法
    public void preOrder() {
        //先输出当前节点
        System.out.println(this);
        //再输出左子节点(递归)——递归向左子树前序遍历
        if (this.left != null) {
            this.left.preOrder();
        }
        //最后输出右子节点(递归)——递归向右子树前序遍历
        if (this.right != null) {
            this.right.preOrder();
        }
    }

    //编写中序遍历的方法
    public void infixOrder() {
        //输出左子节点(递归)——递归向左子树中序遍历
        if (this.left != null) {
            this.left.infixOrder();
        }
        //输出父节点
        System.out.println(this);
        //输出右子节点(递归)——递归向右子树中序遍历
        if (this.right != null) {
            this.right.infixOrder();
        }
    }

    //编写后序遍历的方法
    public void postOrder() {
        //输出左子节点(递归)——递归向左子树后序遍历
        if (this.left != null) {
            this.left.postOrder();
        }
        //输出右子节点(递归)——递归向右子树后序遍历
        if (this.right != null) {
            this.right.postOrder();
        }
        //输出父节点
        System.out.println(this);
    }

    //

    /**
     * 前序遍历查找
     * 如果找到就返回该Node,如果没有找到返回null
     *
     * @MethodName: preOrderSearch
     * @Author: AllenSun
     * @Date: 2019/11/4 23:24
     */
    public HeroNode preOrderSearch(int no) {
        System.out.println("进入前序遍历");//这句话用来统计一共进行了几次前序遍历

        //比较当前结点是不是要找的节点
        if (this.no == no) {
            return this;
        }
        //1-则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
        //2-如果左递归前序查找,找到节点,则返回
        HeroNode resNode = null;
        if (this.left != null) {
            resNode = this.left.preOrderSearch(no);
        }
        if (resNode != null) {
            //说明我们左子树找到,得到节点,则返回,否则继续判断
            return resNode;
        }
        //如果左递归没有找到,就判断判断当前结点的右子节点是否为空,如果不为空,则继续向右递归前序查找
        if (this.right != null) {
            resNode = this.right.preOrderSearch(no);
        }
        return resNode;
    }

    /**
     * 中序遍历查找
     *
     * @MethodName:
     * @Author: AllenSun
     * @Date: 2019/11/4 23:41
     */
    public HeroNode infixOrderSearch(int no) {
        //判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
        HeroNode resNode = null;
        if (this.left != null) {
            resNode = this.left.infixOrderSearch(no);
        }
        if (resNode != null) {
            return resNode;
        }
        System.out.println("进入中序遍历");//这句话用来统计一共进行了几次中序遍历
        //如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
        if (this.no == no) {
            return this;
        }
        //否则继续进行右递归的中序查找
        if (this.right != null) {
            resNode = this.right.infixOrderSearch(no);
        }
        return resNode;
    }

    /**
     * 后序遍历
     *
     * @MethodName:
     * @Author: AllenSun
     * @Date: 2019/11/4 23:59
     */
    public HeroNode postOrderSearch(int no) {
        //判断当前结点左子节点是否为空,如果不为空,则递归后序查找
        HeroNode resNode = null;
        if (this.left != null) {
            resNode = this.left.postOrderSearch(no);
        }
        if (resNode != null) {//说明在左子树找到
            return resNode;
        }
        //如果左子树没有找到,那就到右子树去找递归查找
        if (this.right != null) {
            resNode = this.right.postOrderSearch(no);
        }
        if (resNode != null) {
            return resNode;
        }
        System.out.println("进入后序遍历");//这句话用来统计一共进行了几次后序遍历
        //如果左右子树都没有找到,那就比较当前的节点
        if (this.no == no) {
            return this;
        }
        return resNode;
    }

}

创建一个线索二叉树,实现了线索化功能的二叉树

//创建一个线索二叉树,实现了线索化功能的二叉树
class ThreadedBinaryTree {
    private HeroNode root;//根节点

    public void setRoot(HeroNode root) {
        this.root = root;
    }

    //为了实现线索化,需要创建要给指向当前节点的前驱节点的指针
    //在递归进行线索化时,pre 总是保留前一个节点
    private HeroNode pre = null;

    //重载一把threadedNodes方法
    public void threadedNodes() {
        this.threadedNodes(root);
    }

    //遍历线索化二叉树的方法
    public void threadedList() {
        //定义一个变量,存储当前遍历的节点,从root开始
        HeroNode node = root;
        while (node != null) {
            //循环的找到leftType==1的节点,第一个找到就是8节点
            //后面随着遍历而变化,因为当leftType==1时,说明该节点是按照线索化处理后的有效节点
            while (node.getLeftType() == 0) {
                node = node.getLeft();
            }
            //打印当前这个节点
            System.out.println(node);
            //如果当前节点的右指针指向的是后继节点,就一直输出
            while (node.getRightType() == 1) {
                //获取到当前节点和后继节点
                node = node.getRight();
                System.out.println(node);
            }
            //替换这个遍历的节点
            node=node.getRight();

        }
    }

    //编写对二叉树进行中序线索化的方法
    public void threadedNodes(HeroNode node) {
        //如果node==null,不能线索化
        if (node == null) {
            return;
        }
        //(一)先线索化左子树
        threadedNodes(node.getLeft());
        //(二)线索化当前节点(有难度)
        //处理当前节点的前去节点
        if (node.getLeft() == null) {
            //就让当前节点的左指针指向前驱节点
            node.setLeft(pre);
            //修改当前节点的左指针的类型,指向前驱节点
            node.setLeftType(1);
        }
        //处理后继节点
        if (pre != null && pre.getRight() == null) {
            //让前驱节点的右指针指向当前节点
            pre.setRight(node);
            //修改前驱节点的右指针类型
            pre.setRightType(1);
        }

        //!!!每处理一个节点后,让当前节点是下一个节点的前驱节点
        pre = node;

        //(三)再线索化右子树
        threadedNodes(node.getRight());

    }

    //删除节点
    public void delNode(int no) {
        if (root != null) {
            //如果只有根节点,这里理解判断root是不是就是要删除节点
            if (root.getNo() == no) {
                root = null;
            } else {
                //递归删除
                root.delNode(no);
            }
        } else {
            System.out.println("空树,不能删除");
        }
    }

    //前序遍历
    public void preOrder() {
        if (this.root != null) {
            this.root.preOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    //中序遍历
    public void infixOrder() {
        if (this.root != null) {
            this.root.infixOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    //后序遍历
    public void postOrder() {
        if (this.root != null) {
            this.root.postOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    //前序遍历查找
    public HeroNode preOrderSearch(int no) {
        if (root != null) {
            return root.preOrderSearch(no);
        } else {
            return null;
        }
    }

    //中序遍历查找
    public HeroNode infixOrderSearch(int no) {
        if (root != null) {
            return root.infixOrderSearch(no);
        } else {
            return null;
        }
    }

    //后序遍历查找
    public HeroNode postOrderSearch(int no) {
        if (root != null) {
            return root.postOrderSearch(no);
        } else {
            return null;
        }
    }
}

测试类

public class ThreadedBinaryTreeDemo {
    public static void main(String[] args) {
        //测试一下中序线索二叉树的功能
        HeroNode root = new HeroNode(1, "tom");
        HeroNode node2 = new HeroNode(3, "jack");
        HeroNode node3 = new HeroNode(6, "smith");
        HeroNode node4 = new HeroNode(8, "marry");
        HeroNode node5 = new HeroNode(10, "king");
        HeroNode node6 = new HeroNode(14, "dim");

        //手动创建二叉树
        root.setLeft(node2);
        root.setRight(node3);
        node2.setLeft(node4);
        node2.setRight(node5);
        node3.setLeft(node6);

        //测试线索化
        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
        threadedBinaryTree.setRoot(root);
        threadedBinaryTree.threadedNodes();

        //测试:以10号节点测试
        HeroNode leftNode = node5.getLeft();
        HeroNode rightNode = node5.getRight();
        System.out.println("10号节点的前驱节点是:" + leftNode);
        System.out.println("10号节点的后继节点是:" + rightNode);

        //当线索化二叉树后,能在使用原来的遍历方法
        System.out.println("使用线索化的方式遍历线索化二叉树");
        threadedBinaryTree.threadedList();

    }
}

五、红黑树

红黑树的特点

红黑树是一种自平衡的二叉查找树,除了符合二叉查找树的基本特性,还会有一些新增的附加特性
1-每个节点非红即黑
2-根节点总是黑色的
3-每个叶子节点都是黑色的空节点(NIL节点)
4-如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
5-从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

红黑树的应用

TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树

为什么要用红黑树

红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
在这里插入图片描述

二叉查找树的优点:

试着查找值为10的结点
先从根节点找,10比9大所以去右子树找,10比13小所以去左子树找,10比11小所以去左子树找,最后找到了值为10的节点。这样的话就是使用二分查找的思想,查找所需的最大次数等同于二叉查找树的高度,上面这个树的高度是4,查找一个值最多也就查找4次
在插入节点的时候也是利用类似的方法,通过一层一层的比较大小,找到新节点适合插入的位置

二叉查找树的缺点:

当你插入节点的时候就会发现它的缺陷
如果插入的值是递减的7654321,因为越来越小,所以不断的往左边子树插,这样左边的单条越来越长,就变成了一个瘸子,也就是失去了平衡性,这样查找的时候比较的次数就会大大增加
也就是说如果使用二叉查找树插入的是一个有序序列,那么二叉排序树就会退化成一个链表。所以红黑树可以让树尽可能的保持平衡,降低树的高度,树的查找性取决于树的高度。
漫画:什么是红黑树?

总结

总结一下:思路就是刚添加或者删除的时候需要变色一次,变完以后要通过旋转把左右两边的子树变成平衡的,哪边少就往哪边转,当平衡性解决以后再统一的把颜色变成符合规则的
变色->左旋转->变色->右旋转->变色

六、B-树,B+树,B*树

简介

B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance)
1- B+树的叶子节点链表结构相比于B-树便于扫库,和范围检索
2- B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因
3- B树是B+树的变体,B树分配新节点的概率比B+树要低,空间使用率更高

动态查找树主要有二叉查找树,平衡二叉查找树,红黑树。都是典型的二叉查找树结构,查找的时间复杂度 O(log2-N) 与树的深度相关,降低树的深度就能提高查找的效率,于是就有了多路的 B-tree/B±tree/ B*-tree (B~Tree)。

B树和B树两种变体的区分:
相比于B树,B+树不维护关键字具体信息,不考虑value的存储,所有的我们需要的细细都在叶子节点上

B*树在B+树以及B树的基础上增加了非叶子节点兄弟间的指针,在某些场景效率会更高
主要掌握B树的操作,也就掌握了这两种变体树的操作

详解B树

是为了磁盘或者其他存储设备而设计的一种多叉平衡查找树

B树的接点结构

B树中每个结点包含:
本节点所含关键字个数
指向父结点的指针
关键字
指向子节点的指针数组

#define Max l000 //结点中关键字的最大数目:Max=m-1,m是B-树的阶
#define Min 500 //非根结点中关键字的最小数目:Min=m/2-1
typedef int KeyType; //KeyType关键字类型由用户定义
typedef struct node{ //结点定义中省略了指向关键字代表的记录的指针
   int keynum; //结点中当前拥有的关键字的个数,keynum<<Max
   KeyType key[Max+1]//关键字向量为key[1..keynum],key[0]不用。
   struct node *parent; //指向双亲结点
   struct node *son[Max+1]//指向孩子结点的指针数组,孩子指针向量为son[0..keynum]
}BTreeNode;
typedef BTreeNode *BTree;
B树的特点
  • B-tree是一种多路搜索树(并不是二叉的),对于一棵M阶树:
  • 定义任意非叶子结点最多只有M个孩子;且M>2;
  • 根结点的孩子数为[2, M],除非根结点为叶子节点;
  • 除根结点以外的非叶子结点的儿子数为[M/2, M];
  • 非叶子结点的关键字个数=指向儿子的指针个数-1;
  • 每个非叶子结点存放至少M/2-1(取上整)和至多M-1个关键字;
  • 非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
  • 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
  • 所有叶子结点位于同一层;
    以M=3的一棵3阶B树为例:
    在这里插入图片描述一棵包含了24个英文字母的5阶B树的结构:
    在这里插入图片描述
B树高度与复杂度

在这里插入图片描述

B树的基本操作

查找操作

在B-树中查找给定关键字的方法类似于二叉排序树上的查找。不同的是在每个结点上确定向下查找的路径不一定是二路而是keynum+1路的。

对结点内的存放有序关键字序列的向量key[l…keynum] 用顺序查找或折半查找方法查找。若在某结点内找到待查的关键字K,则返回该结点的地址及K在key[1…keynum]中的位置;否则,确定K在某个key[i]和key[i+1]之间结点后,从磁盘中读son[i]所指的结点继续查找。直到在某结点中查找成功;或直至找到叶结点且叶结点中的查找仍不成功时,查找过程失败。

BTreeNode *SearchBTree(BTree T,KeyType K,int *pos)
{ //在B-树T中查找关键字K,成功时返回找到的结点的地址及K在其中的位置*pos
//失败则返回NULL,且*pos无定义
  int i;
  T→key[0]=k; //设哨兵.下面用顺序查找key[1..keynum]
  for(i=T->keynum;K<t->key[i];i--)//从后向前找第1个小于等于K的关键字
  if(i>0 && T->key[i]==1){ //查找成功,返回T及i
    *pos=i;
    return T;
   } //结点内查找失败,但T->key[i]<K<T->key[i+1],下一个查找的结点应为
     //son[i]
  if(!T->son[i]) //*T为叶子,在叶子中仍未找到K,则整个查找过程失败
    return NULL;
    //查找插入关键字的位置,则应令*pos=i,并返回T,见后面的插入操作
  DiskRead(T->son[i])//在磁盘上读人下一查找的树结点到内存中
  return SearchBTree(T->Son[i],k,pos)//递归地继续查找于树T->son[i]
}
查找操作的时间开销

B-树上的查找有两个基本步骤:
1.在B-树中查找结点,该查找涉及读盘DiskRead操作,属外查找;
2.在结点内查找,该查找属内查找。

查找操作的时间为:
1.外查找的读盘次数不超过树高h,故其时间是O(h);
2.内查找中,每个结点内的关键字数目keynum<m(m是B-树的阶数),故其时间为O(nh)。

注意:
1.实际上外查找时间可能远远大于内查找时间。
2.B-树作为数据库文件时,打开文件之后就必须将根结点读人内存,而直至文件关闭之前,此根一直驻留在内存中,故查找时可以不计读入根结点的时间。

插入操作

插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。

删除操作

首先查找B树中需删除的元素,如果该元素在B树中存在,则将该元素在其结点中进行删除,如果删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素到父节点中,然后是移动之后的情况;如果没有,直接删除后,移动之后的情况。

B+树

B+树和B树的对比区别:

一棵m阶的B+树和m阶的B树的异同点在于:
1.有n棵子树的结点中含有n-1 个关键字;
2.所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (而B 树的叶子节点并没有包括全部需要查找的信息)
3.所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)

为什么说B+树比B树更适合实际应用中操作系统的文件索引和数据库索引:
*
B±tree的磁盘读写代价更低

B±tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。
如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。
一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。
一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
*
B±tree的查询效率更加稳定

由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

B+树的应用:

在使用MySQL时,如果发现系统某个sql查询速度非常慢,可以先检查一下条件字段有没有索引,没有的话可以创建索引,查询速度就会大大提升。
索引用到的数据结构就是B+树,查询时间为log(n),如果用hash存储索引,那么查询时间是平均时间o(1)。显然hash比B+树要快,但是MySQL还是选择用B+树来存储索引,树里面无非就是前中后序遍历、二叉树、二叉搜索树、平衡二叉树、更高级一点有红黑树、B树等等。其实讲到红黑树和B树这些数据结构,重要的不是它的定义或者手写一个红黑树,更重要的是了解它这么设计的思路和原因,学习一个技术,理解背后的设计理念、原理和解决问题的方法,远远比技术本身是什么更重要。

B树是一种多路搜索树,它的每个节点可以拥有多于两个孩子节点。M路的B树最多能拥有M个孩子节点。
为什么要设计成多路呢?因为路数越多,树的高度就越低,进一步的降低树的高度,提高查找性能。
但是如果设计成无限多路的话,就退化成一个数组。
考虑一下B树的使用场景,B树一般是用在做文件系统的索引。为什么文件系统的索引细化用B树而不用红黑树或者有序数组呢?
因为文件系统和数据库的索引都是存在硬盘上的,并且如果数据量大的话,不一定能一次性加载到内存里,如果没法一次性加载完那就先加载B树的一个节点,然后一步步往下找,这样B树的多路存储威力就出来了。假设内存一次性只能加载2个数,这么长的有序数组是没法一次性加载进内存的,我们把它组织成一颗三路的B树,这样每个节点最多有2个数,查找的时候,每次载入一个节点里的两个数加载进内存就行了。
如果在内存里,红黑树比B树效率更高,但是涉及到磁盘操作,B树就更有效率了。

B+树是在B树的基础上进行改造,它的数据都在叶子节点上,同时叶子节点之间还加了指针形成链表。
想一下B+树的应用场景,B+树在数据库的索引中用的比较多,数据库里select数据,不一定只选一条,很多时候会选很多条,比如按照id排序后选10条,如果是多条数据的话,B树还要做局部的中序遍历,可能要跨层访问。而B+树因为所有数据都在叶子节点里,不用去跨层查找,同时因为有链表结构,只需要找到首尾,通过链表就能把所有数据都取出来了。
所以说即使hash的查找速度比B+树要快,但是数据库的索引还是选择使用B+树,就是因为查找的时候会查找很多条数据,如果只是一条数据的话,肯定是hash要快,但是查询多条数据的时候,B+树索引有序,并且又有链表相连,它的效率就比hash快很多。而且数据库的索引一般都是在磁盘上,数据量大的时候可能没法一次装入内存,B+树的设计允许数据分批加载,同时树的高低也较低,提高查找效率。

B*树

B*-tree是B±tree的变体,在B+树的基础上(所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),
B树中非根和非叶子结点再增加指向兄弟的指针;
B
树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2)。

下图是一棵典型的B*树:

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值