《大话数据结构》6、7树、图

第6章树 149

树:
树 (Tree) 是 n (n>=0) 个结点的有限集 。 n=0 时称为空树. 在任意一棵非空 树中:
( 1 ) 有旦仅有一个特定的称为根 (Root) 的结点:
(2) 当 n>1 时,其 余结点可分为 m (m>0) 个互不相交的有限集 T1 T2、 ……、 Tn, 其中每一个 集合本身又是一棵树,并且称为根的子树 (SubTree)。

6.1开场白 150

无论多高多大的树,那也是从小到大的,由根到叶,一点点成长起来的。俗话说十年树木,百年树人,可一棵大树又何止是十年这样容易。

为什么需要树这种数据结构
数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低 [示意图]
在这里插入图片描述
在这里插入图片描述
链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历) 【示意图】

树存储方式的分析能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。【示意图,后面详讲】案例: [7, 3, 10, 1, 5, 9, 12]
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二叉树的概念

树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树。
二叉树的子节点分为左节点和右节点。
在这里插入图片描述
如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
在这里插入图片描述

我们重点讲解一下二叉树的前序遍历,中序遍历和后序遍历。

二叉树遍历

使用前序,中序和后序对下面的二叉树进行遍历.
在这里插入图片描述
前序遍历: 先输出父节点,再遍历左子树和右子树
中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
小结: 看输出父节点的顺序,就确定是前序,中序还是后序
在这里插入图片描述
二叉树遍历应用实例(前序,中序,后序)
在这里插入图片描述
要求如下:
前上图的 3号节点 “卢俊” , 增加一个左子节点 [5, 关胜]
使用前序,中序,后序遍历,请写出各自输出的顺序是什么?

package com.zcr.tree;

/**
 * @author zcr
 * @date 2019/7/8-10:32
 */
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,"关胜");

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

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


    }
}

//先创建HeroNode节点
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 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);
    }
}

//定义二叉树
class BinaryTree {
    private HeroNode root;

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

    //前序遍历
    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("二叉树为空,无法遍历");
        }
    }
}

二叉树-查找指定节点

要求
请编写前序查找,中序查找和后序查找的方法。
并分别使用三种查找方式,查找 heroNO = 5 的节点
并分析各种查找方式,分别比较了多少次
在这里插入图片描述
package com.zcr.tree;

/**
 * @author zcr
 * @date 2019/7/8-10:32
 */
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,"关胜");

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

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

        //测试查找
        System.out.println("前序遍历查找");
        HeroNode resNode = binaryTree.preOrderSearch(5);
        if (resNode != null) {
            System.out.printf("找到了,信息为no=%d name=%s",resNode.getNo(),resNode.getName());
        } else {
            System.out.printf("没有找到no=%d的英雄",5);
        }
        System.out.println();

        System.out.println("中序遍历查找");
        resNode = binaryTree.infixOrderSearch(5);
        if (resNode != null) {
            System.out.printf("找到了,信息为no=%d name=%s",resNode.getNo(),resNode.getName());
        } else {
            System.out.printf("没有找到no=%d的英雄",5);
        }
        System.out.println();

        System.out.println("后序遍历查找");
        resNode = binaryTree.postOrderSearch(5);
        if (resNode != null) {
            System.out.printf("找到了,信息为no=%d name=%s",resNode.getNo(),resNode.getName());
        } else {
            System.out.printf("没有找到no=%d的英雄",5);
        }


    }
}

//先创建HeroNode节点
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 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);
    }

    //前序遍历查找
    public HeroNode preOrderSearch(int no) {
        System.out.println("进入前序遍历查找~~");
        //比较当前节点是不是
        if (this.no == no) {
            return this;
        }
        //则判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
        //如果左递归前序查找找到了节点,则返回
        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;//向右不管找没找到都返回
    }

    //中序遍历查找
    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;
    }

    //后序遍历查找
    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 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;
        }
    }
}

二叉树-删除节点

要求
如果删除的节点是叶子节点,则删除该节点
如果删除的节点是非叶子节点,则删除该子树.
测试,删除掉 5号叶子节点 和 3号子树.
在这里插入图片描述
二叉树-删除节点
思考题(课后练习)
如果要删除的节点是非叶子节点,现在我们不希望将该非叶子节点为根节点的子树删除,需要指定规则, 假如规定如下:
如果该非叶子节点A只有一个子节点B,则子节点B替代节点A
如果该非叶子节点A有左子节点B和右子节点C,则让左子节点B替代节点A。
请大家思考,如何完成该删除功能, 老师给出提示.(课后练习)
后面在讲解 二叉排序树时,在给大家讲解具体的删除方法
在这里插入图片描述

package com.zcr.tree;

/**
 * @author zcr
 * @date 2019/7/8-10:32
 */
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,"关胜");

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

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

        //测试查找
        System.out.println("前序遍历查找");
        HeroNode resNode = binaryTree.preOrderSearch(5);
        if (resNode != null) {
            System.out.printf("找到了,信息为no=%d name=%s",resNode.getNo(),resNode.getName());
        } else {
            System.out.printf("没有找到no=%d的英雄",5);
        }
        System.out.println();

        System.out.println("中序遍历查找");
        resNode = binaryTree.infixOrderSearch(5);
        if (resNode != null) {
            System.out.printf("找到了,信息为no=%d name=%s",resNode.getNo(),resNode.getName());
        } else {
            System.out.printf("没有找到no=%d的英雄",5);
        }
        System.out.println();

        System.out.println("后序遍历查找");
        resNode = binaryTree.postOrderSearch(5);
        if (resNode != null) {
            System.out.printf("找到了,信息为no=%d name=%s",resNode.getNo(),resNode.getName());
        } else {
            System.out.printf("没有找到no=%d的英雄",5);
        }

        //删除节点
        System.out.println("删除前,前序遍历情况:");
        binaryTree.preOrder();
        binaryTree.delNode(3);
        System.out.println("删除后,前序遍历情况:");
        binaryTree.preOrder();



    }
}

//先创建HeroNode节点
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 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);
    }

    //前序遍历查找
    public HeroNode preOrderSearch(int no) {
        System.out.println("进入前序遍历查找~~");
        //比较当前节点是不是
        if (this.no == no) {
            return this;
        }
        //则判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
        //如果左递归前序查找找到了节点,则返回
        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;//向右不管找没找到都返回
    }

    //中序遍历查找
    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;
    }

    //后序遍历查找
    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;
    }

    //递归删除节点
    //如果删除的的节点是叶子节点,则删除该节点
    //如果删除的节点是非叶子节点,则删除该子树
    public void delNode(int no) {
        //思路
        //1.因为我们的二叉树是单向的,所以我们是判断当前节点的子节点是否需要删除节点,而不能去判断当前这个节点是不是需要删除节点
        //2.如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,那么就将其置为空,并且返回
        //3.如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,那么就将其置为空,并且返回
        //4.如果第2,3步没有删除节点,那么我们呢就需要向左子树进行递归删除
        //5.如果第4步也没有删除节点,则应当向右子树进行递归删除
        if (this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        if (this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        if (this.left != null) {
            this.left.delNode(no);//因为可能不成功,所以这里没有return
        }
        if (this.right != null) {
            this.right.delNode(no);
        }
    }

}

//定义二叉树
class BinaryTree {
    private HeroNode root;

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

    //前序遍历
    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 void delNode(int no) {
        if (root != null) {
            //如果只有一个root节点,这里立即判断root是不是就是要删除的节点
            if (root.getNo() == no) {
                root = null;
            } else {
                //递归删除
                root.delNode(no);
            }
        } else {
            System.out.println("空树,不能删除");
        }
    }
}

顺序存储二叉树的概念

基本说明
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,
看右面的示意图。
要求:
右图的二叉树的结点,要求以数组的方式来存放 arr : [1, 2, 3, 4, 5, 6, 6]要求在遍历数组 arr时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成结点的遍历
在这里插入图片描述
顺序存储二叉树的特点:
顺序二叉树通常只考虑完全二叉树
第n个元素的左子节点为 2 * n + 1
第n个元素的右子节点为 2 * n + 2
第n个元素的父节点为 (n-1) / 2
n : 表示二叉树中的第几个元素(按0开始编号如图所示)

顺序存储二叉树遍历
需求: 给你一个数组 {1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。 前序遍历的结果应当为 1,2,4,5,3,6,7
课后练习:请同学们完成对数组以二叉树中序,后序遍历方式的代码.

package com.zcr.tree;

/**
 * @author zcr
 * @date 2019/7/8-12:13
 */
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();//1,2,4,5,3,6,7
    }
}

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

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

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

    //完成顺序存储二叉树的前序遍历
    /**
     *
     * @param index 数组的下标
     */
    public void preOrder(int index) {
        //如果数组为空,或者arr.length = 0
        if (arr == null || arr.length == 0) {
            System.out.println("数组为空,不能按照二叉树的前序遍历");
        }
        //输出当前这个元素
        System.out.println(arr[index]);
        //向左递归遍历
        if ((index * 2 + 1) < arr.length) {
            preOrder(2 * index + 1);
        }
        //向右递归遍历
        if ((index * 2 + 2) < arr.length) {
            preOrder(2 * index + 2);
        }


    }
}

顺序存储二叉树应用实例
八大排序算法中的堆排序,就会使用到顺序存储二叉树, 关于堆排序,我们放在<<树结构实际应用>> 章节讲解。

线索化二叉树

先看一个问题
将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树. n+1=7
在这里插入图片描述
问题分析:
当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }
但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上.
如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
解决方案-线索二叉树

线索二叉树基本介绍
n个结点的二叉链表中含有n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
一个结点的前一个结点,称为前驱结点
一个结点的后一个结点,称为后继结点

线索二叉树应用案例
应用案例说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}
在这里插入图片描述
线索二叉树应用案例
思路分析: 中序遍历的结果:{8, 3, 10, 1, 14, 6}
在这里插入图片描述
说明: 当线索化二叉树后,Node节点的 属性 left 和 right ,有如下情况:
left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点.
right指向的是右子树,也可能是指向后继节点,比如 ① 节点right 指向的是右子树,而⑩ 节点的right 指向的是后继节点.

在这里插入图片描述
在这里插入图片描述
遍历线索化二叉树
说明:对前面的中序线索化的二叉树, 进行遍历
分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历的次序应当和中序遍历保持一致。
代码:

package com.zcr.tree;

/**
 * @author zcr
 * @date 2019/7/8-12:36
 */
public class ThreadedBinaryTreeDemo {

    public static void main(String[] args) {

        //测试中序线索二叉树的功能

        //创建节点
        Node root= new Node(1,"tom");
        Node node2= new Node(3,"jack");
        Node node3= new Node(6,"smith");
        Node node4= new Node(8,"mary");
        Node node5= new Node(10,"king");
        Node node6= new Node(14,"dim");

        //创建二叉树,先手动创建,后面我们递归创建
        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
        threadedBinaryTree.setRoot(root);
        root.setLeft(node2);
        root.setRight(node3);
        node2.setLeft(node4);
        node2.setRight(node5);
        node3.setLeft(node6);

        //中序线索化
        threadedBinaryTree.threadedNodes();

        //看看10这个点的前驱和后继节点是谁
        Node leftnode = node5.getLeft();
        Node rightnode = node5.getRight();
        System.out.println("10号节点的前驱节点是:"+leftnode);
        System.out.println("10号节点的后驱节点是:"+rightnode);

        //对中序线索化后的二叉树进行遍历
        System.out.println("使用线索化的方式遍历线索化二叉树:");
        threadedBinaryTree.threadedList();//8,3,10,1,14,6


    }
}

//先创建HeroNode节点
class Node {
    private int no;
    private String name;
    private Node left;//默认为空
    private Node right;//默认为空
    private int leftType;//0表示指向左子树 1表示指向前驱节点
    private int rightType;//0表示指向右子树 1表示指向后继节点

    public Node(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 Node getLeft() {
        return left;
    }

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

    public Node getRight() {
        return right;
    }

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

    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;
    }

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

    //前序遍历
    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);
    }
}



//定义二叉树,实现了线索化功能的二叉树,即ThreadedBinaryTree
class ThreadedBinaryTree {
    private Node root;

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

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

    //重载线索化的方法
    public void threadedNodes() {
        this.threadedNodes(root);
    }

    //编写对二叉树进行中序线索化的方法
    /**
     *
     * @param node 就是当前进行线索化的节点
     */
    public void threadedNodes(Node 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 threadedList() {
        //定义一个变量,存储当前遍历的节点,从root开始
        Node 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 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("二叉树为空,无法遍历");
        }
    }


}

线索二叉树应用案例
课后作业:
我这里讲解了中序线索化二叉树,前序线索化二叉树和后序线索化二叉树的分析思路类似,同学们作为课后作业完成.

堆排序

堆排序基本介绍
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
大顶堆举例说明
在这里插入图片描述
在这里插入图片描述
我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子:
大顶堆特点:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 对应第几个节点,i从0开始编号

小顶堆举例说明
在这里插入图片描述
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 对应第几个节点,i从0开始编号
一般升序采用大顶堆,降序采用小顶堆

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

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.

堆排序步骤图解说明
要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

堆排序代码实现

要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。
代码实现:看老师演示:
说明:
堆排序不是很好理解,老师通过Debug 帮助大家理解堆排序
堆排序的速度非常快,在我的机器上 8百万数据 3 秒左右。O(nlogn)

package com.zcr.tree;

import java.util.Arrays;

/**
 * @author zcr
 * @date 2019/7/8-16:56
 */
public class HeapSort {

    public static void main(String[] args) {
        int arr[] = {4,6,8,5,9,-1,90,89,56,-999};
        heapSort(arr);
    }

    //编写堆排序的方法
    public static void heapSort(int arr[]) {
        int temp = 0;
        System.out.println("堆排序");

        /*//分步完成
        adjustHeap(arr,1,arr.length);
        System.out.println("第一次调整后"+ Arrays.toString(arr));//[4,9,8,5,6]

        adjustHeap(arr,0,arr.length);
        System.out.println("第二次调整后"+Arrays.toString(arr));//[9,6,8,5,4]
*/
        //将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
        for (int i = arr.length / 2 -1; i >= 0 ; i--) {
            adjustHeap(arr,i,arr.length);
        }
//        System.out.println(Arrays.toString(arr));

        //将堆顶元素与末尾元素交换,将最大元素沉到数组末端
        //重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
        for (int j = arr.length - 1; j > 0; j--) {//调整4个数就够了
            //交换
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            adjustHeap(arr,0,j);//真实的调整总是从顶上开始的
        }
        System.out.println(Arrays.toString(arr));



    }

    //将一个数组(二叉树的顺序存储),调整成一个大顶堆
    /**
     * 功能:完成将以i指向对应的非叶子节点的树调整成大顶堆
     * 举例:{4,6,8,5,9} 第一个非叶子节点是i=1 -》{4,9,8,5,6}-i=0》{9,6,8,5,4}
     * @param arr 待调整的数组
     * @param i 表示非叶子节点在数组中的索引
     * @param length 表示对多少个元素进行调整,length在逐渐减少
     */
    public static void adjustHeap(int arr[],int i,int length) {

        //先取出当前元素的值,保存在临时变量
        int temp = arr[i];
        //开始调整
        //1.k = i * 2 + 1 k是i的左子节点
        //2.k = k * 2 + 1 继续判断k的左子节点
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
            if (k + 1 < length && arr[k] < arr[k + 1]) {//说明左子节点小于右子节点的值
                k++;//k指向右子节点
            }
            if (arr[k] > temp) {//说明子节点大于父节点
                arr[i] = arr[k];//把较大的值赋值给当前节点
                i = k;//让i指向k,继续循环比较(k下面还有左子树或右子树)
            } else {
                break;//从左到右,从下到上逐渐调整的
            }
        }
        //当for循环结束后,我们已经将以i为父节点的树的最大值,放在了最顶上(局部)
        arr[i] = temp;//将temp的值放到调整后的位置
    }
}

赫夫曼树

基本介绍
给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的**带权路径长度(wpl)**达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近

赫夫曼树几个重要概念和举例说明
路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1

结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
在这里插入图片描述
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
WPL最小的就是赫夫曼树
在这里插入图片描述
赫夫曼树创建思路图解

给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.

思路分析(示意图):
{13, 7, 8, 3, 29, 6, 1}
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

构成赫夫曼树的步骤:
1.从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
2.取出根节点权值最小的两颗二叉树
3.组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
4.再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

赫夫曼树的代码实现

代码实现:

package com.zcr.tree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @author zcr
 * @date 2019/7/8-18:16
 */
public class HuffmanTree {

    public static void main(String[] args) {

        int arr[] = {13,7,8,3,29,6,1};
        Node2 noderes = createHuffmanTree(arr);
        System.out.println(noderes);
        //前序遍历创建好的赫夫曼二叉树
        preOrder(noderes);//67 29 38 15 7 8 23 10 4 1 3 6 13



    }

    //创建赫夫曼树的方法

    /**
     *
     * @param arr 需要创建成赫夫曼树的数组
     * @return 创建好的赫夫曼树的根节点
     */
    public static Node2 createHuffmanTree(int[] arr) {
        //第一步:为了操作方便
        //1.遍历arr数组
        //2.将arr的每一个元素构建成一个Node
        //3.将Node放入到ArrayList中
        List<Node2> nodes = new ArrayList<Node2>();
        for (int value : arr) {
            nodes.add(new Node2(value));
        }

        //处理的过程是一个循环的过程!
        while (nodes.size() > 1) {
            //排序,从小到大排序
            Collections.sort(nodes);
            System.out.println("nodes="+nodes);

            //取出根节点权值最小的两颗二叉树
            //1.取出权值最小的二叉树节点【一个节点也可以看做是一颗二叉树】
            Node2 leftNode  = nodes.get(0);
            //2.取出权值第二小的节点
            Node2 rightNode = nodes.get(1);
            //3.构建一颗新的二叉树
            Node2 parent = new Node2(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;
            //4.从arrlist中删除掉处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //5.把新节点加入到Arrlist中
            nodes.add(parent);

            /*//Collections.sort(nodes);
            System.out.println("第一次处理后:"+ nodes);*/
        }
        //返回赫夫曼的root节点
        return nodes.get(0);
    }

    //前序遍历方法
    public static void preOrder(Node2 root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("空树无法遍历");
        }
    }



}

//创建节点类
//为了让Node对象支持排序Collection集合排序
//让Node实现comparable接口
class Node2 implements Comparable<Node2>{
    int value;//节点权值
    Node2 left;//指向左子节点
    Node2 right;//指向右子节点


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

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

    @Override
    public int compareTo(Node2 o) {
        return this.value - o.value;//从小到大排序  自己大,返回正数;自己小,返回负数
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

赫夫曼编码
基本介绍
赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码

原理剖析
通信领域中信息的处理方式1-定长编码
i like like like java do you like a java // 共40个字符(包括空格)
105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97 //对应Ascii码
01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应的二进制
按照二进制来传递信息,总的长度是 359 (包括空格)
在线转码 工具 :https://www.mokuge.com/tool/asciito16/

原理剖析
通信领域中信息的处理方式2-变长编码
i like like like java do you like a java // 共40个字符(包括空格)
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
0= , 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d 说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推.
按照上面给各个字符规定的编码,则我们在传输 “i like like like java do you like a java” 数据时,编码就是 10010110100…
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码(这个在赫夫曼编码中,我们还要进行举例说明, 不捉急)

原理剖析
通信领域中信息的处理方式3-赫夫曼编码
i like like like java do you like a java // 共40个字符(包括空格)
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.(图后)

原理剖析
通信领域中信息的处理方式3-赫夫曼编码
在这里插入图片描述
在这里插入图片描述
原理剖析
注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
在这里插入图片描述
最佳实践-数据压缩(创建赫夫曼树)
将给出的一段文本,比如 “i like like like java do you like a java” , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理 ,形式如 "1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
"
步骤1:根据赫夫曼编码压缩数据的原理,需要创建 “i like like like java do you like a java” 对应的赫夫曼树.
思路:前面已经分析过了,而且我们已然讲过了构建赫夫曼树的具体实现。
代码实现:看老师演示:
在这里插入图片描述

package com.zcr.tree;

import java.util.*;

/**
 * @author zcr
 * @date 2019/7/8-20:49
 */
public class HuffmanCode {

    public static void main(String[] args) {

        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();//字符串转换成字节数组
        System.out.println(contentBytes.length);

        //将字节数组转换成节点数组
        List<Node3> nodes = getNodes(contentBytes);
        System.out.println(nodes);

        //将节点数组生成哈夫曼树
        System.out.println("哈夫曼树:");
        Node3 huffmanTreeRoot = createHuffmanTree(nodes);//哈夫曼树的根节点

        //前序遍历哈夫曼树
        System.out.println("前序遍历哈夫曼树");
        preOrder(huffmanTreeRoot);

        //生成赫夫曼树对应的赫夫曼编码


    }

    /**
     * @param bytes 接收一个字节数组(字符串-》字节数组)
     * @return 返回的就是一个节点列表(Map【值,次数】-》节点列表【值,次数】)
     */
    private static List<Node3> getNodes(byte[] bytes) {
        //1.先创建一个ArrayList
        List<Node3> nodes = new ArrayList<Node3>();


        //2.遍历bytes,统计每一个byte出现的次数,使用map存储
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//map还没有这个字符数据
                counts.put(b, 1);
            } else {//map中已经有了这个字
                counts.put(b, count+1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node3(entry.getKey(), entry.getValue()));
        }
        return nodes;

    }

    //通过节点列表创建对应的huffman树
    private static Node3 createHuffmanTree(List<Node3> nodes) {
        while (nodes.size() > 1) {
            //从小到大排序
            Collections.sort(nodes);
            System.out.println("nodes="+nodes);

            //取出第一颗最小的二叉树
            Node3 leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node3 rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有data,只有权值!(哈夫曼数中只有叶子节点有数据,飞叶子节点没有数据)
            Node3 parent = new Node3(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理过的两颗二叉树移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入
            nodes.add(parent);
        }
        //返回最后的节点就是哈夫曼树的根节点
        return nodes.get(0);

    }

    //前序遍历
    private static void preOrder(Node3 root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("哈夫曼树为空,不能遍历");
        }
    }
}


//创建节点,带数据和权值
class Node3 implements Comparable<Node3>{
    Byte data;//存放数据本身的ASCII码值,'a'->97 ' '->32
    int weight;//权值,表示出字符出现的次数
    Node3 left;
    Node3 right;

    public Node3(Byte data,int weight) {
        this.data = data;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node3 o) {
        return this.weight - o.weight;//从小到大排序
    }

    @Override
    public String toString() {
        return "Node3{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

最佳实践-数据压缩(生成赫夫曼编码和赫夫曼编码后的数据)
我们已经生成了 赫夫曼树, 下面我们继续完成任务
生成赫夫曼树对应的赫夫曼编码 , 如下表:=01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
**使用赫夫曼编码来生成赫夫曼编码数据 ,**即按照上面的赫夫曼编码,将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下.1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
思路:前面已经分析过了,而且我们讲过了生成赫夫曼编码的具体实现。
代码实现:看老师演示:
在这里插入图片描述
在这里插入图片描述

package com.zcr.tree;

import java.util.*;

/**
 * @author zcr
 * @date 2019/7/8-20:49
 */
public class HuffmanCode {

    public static void main(String[] args) {

        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();//字符串转换成字节数组
        System.out.println(contentBytes.length);//40

        //将字节数组转换成节点数组
        List<Node3> nodes = getNodes(contentBytes);
        System.out.println(nodes);

        //将节点数组生成哈夫曼树
        System.out.println("哈夫曼树:");
        Node3 huffmanTreeRoot = createHuffmanTree(nodes);//哈夫曼树的根节点

        //前序遍历哈夫曼树
        System.out.println("前序遍历哈夫曼树");
        preOrder(huffmanTreeRoot);

        //生成赫夫曼树对应的赫夫曼编码
//        getCodes(huffmanTreeRoot,"",stringBuilder);
        getCodes(huffmanTreeRoot);//返回也行不返回也行
        System.out.println("生成的哈夫曼编码表为:"+huffmanCodes);

        //生成字符串的哈夫曼编码
        byte[] huffmanCodeByte = zip(contentBytes,huffmanCodes);//长度133
        System.out.println("压缩后的字符串为:"+Arrays.toString(huffmanCodeByte));//17

    }

    /**
     * @param bytes 接收一个字节数组(字符串-》字节数组)
     * @return 返回的就是一个节点列表(Map【值,次数】-》节点列表【值,次数】)
     */
    private static List<Node3> getNodes(byte[] bytes) {
        //1.先创建一个ArrayList
        List<Node3> nodes = new ArrayList<Node3>();


        //2.遍历bytes,统计每一个byte出现的次数,使用map存储
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//map还没有这个字符数据
                counts.put(b, 1);
            } else {//map中已经有了这个字
                counts.put(b, count+1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node3(entry.getKey(), entry.getValue()));
        }
        return nodes;

    }

    //通过节点列表创建对应的huffman树
    private static Node3 createHuffmanTree(List<Node3> nodes) {
        while (nodes.size() > 1) {
            //从小到大排序
            Collections.sort(nodes);
            System.out.println("nodes="+nodes);

            //取出第一颗最小的二叉树
            Node3 leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node3 rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有data,只有权值!(哈夫曼数中只有叶子节点有数据,飞叶子节点没有数据)
            Node3 parent = new Node3(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理过的两颗二叉树移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入
            nodes.add(parent);
        }
        //返回最后的节点就是哈夫曼树的根节点
        return nodes.get(0);

    }

    //前序遍历
    private static void preOrder(Node3 root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("哈夫曼树为空,不能遍历");
        }
    }

    //为了调用方便,重载getCodes
    private static Map<Byte,String> getCodes(Node3 root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left,"0",stringBuilder);
        //处理root的右子树
        getCodes(root.right,"1",stringBuilder);
        return huffmanCodes;
    }

    //生成哈夫曼树对应的哈夫曼编码
    //1.将哈夫曼编码表存放在Map<Byte,String>形式 32->01 97->100 100->11000....
    static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();
    //2.在生成哈夫曼编码表时,需要去拼接路径,定义一个StringBuilder存储某个叶子节点的路径
    static StringBuilder stringBuilder = new StringBuilder();
    /**
     * 功能:将传入的node节点的所有叶子节点的哈夫曼编码得到,并放入到哈夫曼集合中
     * @param node 传入节点
     * @param code 路径:左子节点是0,右子节点是1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(Node3 node,String code,StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//node为空时不处理
            //判断当前node是叶子节点还是非叶子节点
            if (node.data == null) {//非叶子节点
                //递归处理
                //向左递归
                getCodes(node.left,"0",stringBuilder2);
                //向右递归
                getCodes(node.right,"1",stringBuilder2);
            } else {//叶子节点
                //就表示找到了某个叶子节点的最后
                huffmanCodes.put(node.data,stringBuilder2.toString());
            }
        }
    }

    //将字符串对应的byte[]数组,通过生成的哈夫曼编码表,返回一个哈夫曼编码压缩后的byte[]

    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @param huffmanCodes 生成的哈夫曼编码表map
     * @return 返回哈夫曼编码处理后的对应的字节数组
     */
    private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes) {
        //1.先利用哈夫曼编码表将传进来的原始字节数组先转成哈夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历byte数组
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        System.out.println("哈夫曼编码字符串"+stringBuilder);//133

        //将stringBuilder转成byte[]
        //统计返回byte[] huffmanCodeBytes长度
        //一句话搞定:int len = (stringBuilder.length() + 7) / 8;
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }

        //创建存储压缩后的byte数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;//定义一个计数器,记录是第几个byte
        //遍历stringBuilder,根据它八位八位的放
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            //因为是每8位对应一个byte
            String strByte;
            if (i + 8 > stringBuilder.length()) {//最后,不够8位了
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            //将strByte转成一个byte,放入到huffmanCodeBytes
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

}


//创建节点,带数据和权值
class Node3 implements Comparable<Node3>{
    Byte data;//存放数据本身的ASCII码值,'a'->97 ' '->32
    int weight;//权值,表示出字符出现的次数
    Node3 left;
    Node3 right;

    public Node3(Byte data,int weight) {
        this.data = data;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node3 o) {
        return this.weight - o.weight;//从小到大排序
    }

    @Override
    public String toString() {
        return "Node3{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

封装后的代码

package com.zcr.tree;

import java.util.*;

/**
 * @author zcr
 * @date 2019/7/8-20:49
 */
public class HuffmanCode {

    public static void main(String[] args) {

        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();//字符串转换成字节数组
        System.out.println(contentBytes.length);//40

        /*//分步骤过程
        //将字节数组转换成节点数组
        List<Node3> nodes = getNodes(contentBytes);
        System.out.println(nodes);

        //将节点数组生成哈夫曼树
        System.out.println("哈夫曼树:");
        Node3 huffmanTreeRoot = createHuffmanTree(nodes);//哈夫曼树的根节点

        //前序遍历哈夫曼树
        System.out.println("前序遍历哈夫曼树");
        preOrder(huffmanTreeRoot);

        //生成赫夫曼树对应的赫夫曼编码
//        getCodes(huffmanTreeRoot,"",stringBuilder);
        getCodes(huffmanTreeRoot);//返回也行不返回也行
        System.out.println("生成的哈夫曼编码表为:"+huffmanCodes);

        //生成字符串的哈夫曼编码
        byte[] huffmanCodeByte = zip(contentBytes,huffmanCodes);//长度133
        System.out.println("压缩后的字符串为:"+Arrays.toString(huffmanCodeByte));//17
*/

        byte[] huffmanCodeBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果是"+Arrays.toString(huffmanCodeBytes));
    }

    /**
     * @param bytes 接收一个字节数组(字符串-》字节数组)
     * @return 返回的就是一个节点列表(Map【值,次数】-》节点列表【值,次数】)
     */
    private static List<Node3> getNodes(byte[] bytes) {
        //1.先创建一个ArrayList
        List<Node3> nodes = new ArrayList<Node3>();


        //2.遍历bytes,统计每一个byte出现的次数,使用map存储
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//map还没有这个字符数据
                counts.put(b, 1);
            } else {//map中已经有了这个字
                counts.put(b, count+1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node3(entry.getKey(), entry.getValue()));
        }
        return nodes;

    }

    //通过节点列表创建对应的huffman树
    private static Node3 createHuffmanTree(List<Node3> nodes) {
        while (nodes.size() > 1) {
            //从小到大排序
            Collections.sort(nodes);
            System.out.println("nodes="+nodes);

            //取出第一颗最小的二叉树
            Node3 leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node3 rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有data,只有权值!(哈夫曼数中只有叶子节点有数据,飞叶子节点没有数据)
            Node3 parent = new Node3(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理过的两颗二叉树移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入
            nodes.add(parent);
        }
        //返回最后的节点就是哈夫曼树的根节点
        return nodes.get(0);

    }

    //前序遍历
    private static void preOrder(Node3 root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("哈夫曼树为空,不能遍历");
        }
    }

    //为了调用方便,重载getCodes
    private static Map<Byte,String> getCodes(Node3 root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left,"0",stringBuilder);
        //处理root的右子树
        getCodes(root.right,"1",stringBuilder);
        return huffmanCodes;
    }

    //生成哈夫曼树对应的哈夫曼编码
    //1.将哈夫曼编码表存放在Map<Byte,String>形式 32->01 97->100 100->11000....
    static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();
    //2.在生成哈夫曼编码表时,需要去拼接路径,定义一个StringBuilder存储某个叶子节点的路径
    static StringBuilder stringBuilder = new StringBuilder();
    /**
     * 功能:将传入的node节点的所有叶子节点的哈夫曼编码得到,并放入到哈夫曼集合中
     * @param node 传入节点
     * @param code 路径:左子节点是0,右子节点是1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(Node3 node,String code,StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//node为空时不处理
            //判断当前node是叶子节点还是非叶子节点
            if (node.data == null) {//非叶子节点
                //递归处理
                //向左递归
                getCodes(node.left,"0",stringBuilder2);
                //向右递归
                getCodes(node.right,"1",stringBuilder2);
            } else {//叶子节点
                //就表示找到了某个叶子节点的最后
                huffmanCodes.put(node.data,stringBuilder2.toString());
            }
        }
    }

    //将字符串对应的byte[]数组,通过生成的哈夫曼编码表,返回一个哈夫曼编码压缩后的byte[]

    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @param huffmanCodes 生成的哈夫曼编码表map
     * @return 返回哈夫曼编码处理后的对应的字节数组
     */
    private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes) {
        //1.先利用哈夫曼编码表将传进来的原始字节数组先转成哈夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历byte数组
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        System.out.println("哈夫曼编码字符串"+stringBuilder);//133

        //将stringBuilder转成byte[]
        //统计返回byte[] huffmanCodeBytes长度
        //一句话搞定:int len = (stringBuilder.length() + 7) / 8;
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }

        //创建存储压缩后的byte数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;//定义一个计数器,记录是第几个byte
        //遍历stringBuilder,根据它八位八位的放
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            //因为是每8位对应一个byte
            String strByte;
            if (i + 8 > stringBuilder.length()) {//最后,不够8位了
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            //将strByte转成一个byte,放入到huffmanCodeBytes
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

    //使用一个方法,将前面的方法封装起来,便于我们的调用

    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 经过哈夫曼编码处理后的字节数组(压缩后的数组)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        //1.将字节数组转换成节点数组
        List<Node3> nodes = getNodes(bytes);

        //2.将节点数组生成哈夫曼树
        Node3 huffmanTreeRoot = createHuffmanTree(nodes);//哈夫曼树的根节点

        //3.生成赫夫曼树对应的赫夫曼编码
        Map<Byte,String> huffmanCodes = getCodes(huffmanTreeRoot);//返回也行不返回也行

        //4.根据生成的哈夫曼编码,压缩得到压缩后的哈夫曼编码字节数组
        byte[] huffmanCodeByte = zip(bytes,huffmanCodes);//长度133

        return huffmanCodeByte;

    }

}


//创建节点,带数据和权值
class Node3 implements Comparable<Node3>{
    Byte data;//存放数据本身的ASCII码值,'a'->97 ' '->32
    int weight;//权值,表示出字符出现的次数
    Node3 left;
    Node3 right;

    public Node3(Byte data,int weight) {
        this.data = data;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node3 o) {
        return this.weight - o.weight;//从小到大排序
    }

    @Override
    public String toString() {
        return "Node3{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

最佳实践-数据解压(使用赫夫曼编码解码)
使用赫夫曼编码来解码数据,具体要求是
前面我们得到了赫夫曼编码和对应的编码byte[] , 即:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
现在要求使用赫夫曼编码, 进行解码,又重新得到原来的字符串"i like like like java do you like a java"

思路:解码过程,就是编码的一个逆向操作。
代码实现:看老师演示:

package com.zcr.tree;

import java.util.*;

/**
 * @author zcr
 * @date 2019/7/8-20:49
 */
public class HuffmanCode {

    public static void main(String[] args) {

        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();//字符串转换成字节数组
        System.out.println(contentBytes.length);//40

        /*//分步骤过程
        //将字节数组转换成节点数组
        List<Node3> nodes = getNodes(contentBytes);
        System.out.println(nodes);

        //将节点数组生成哈夫曼树
        System.out.println("哈夫曼树:");
        Node3 huffmanTreeRoot = createHuffmanTree(nodes);//哈夫曼树的根节点

        //前序遍历哈夫曼树
        System.out.println("前序遍历哈夫曼树");
        preOrder(huffmanTreeRoot);

        //生成赫夫曼树对应的赫夫曼编码
//        getCodes(huffmanTreeRoot,"",stringBuilder);
        getCodes(huffmanTreeRoot);//返回也行不返回也行
        System.out.println("生成的哈夫曼编码表为:"+huffmanCodes);

        //生成字符串的哈夫曼编码
        byte[] huffmanCodeByte = zip(contentBytes,huffmanCodes);//长度133
        System.out.println("压缩后的字符串为:"+Arrays.toString(huffmanCodeByte));//17
*/

        byte[] huffmanCodeBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果是"+Arrays.toString(huffmanCodeBytes));

        byte[] sourceBytes = decode(huffmanCodes,huffmanCodeBytes);
        System.out.println("原来的字符串是"+new String(sourceBytes));


    }

    /**
     * @param bytes 接收一个字节数组(字符串-》字节数组)
     * @return 返回的就是一个节点列表(Map【值,次数】-》节点列表【值,次数】)
     */
    private static List<Node3> getNodes(byte[] bytes) {
        //1.先创建一个ArrayList
        List<Node3> nodes = new ArrayList<Node3>();


        //2.遍历bytes,统计每一个byte出现的次数,使用map存储
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//map还没有这个字符数据
                counts.put(b, 1);
            } else {//map中已经有了这个字
                counts.put(b, count+1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node3(entry.getKey(), entry.getValue()));
        }
        return nodes;

    }

    //通过节点列表创建对应的huffman树
    private static Node3 createHuffmanTree(List<Node3> nodes) {
        while (nodes.size() > 1) {
            //从小到大排序
            Collections.sort(nodes);
            System.out.println("nodes="+nodes);

            //取出第一颗最小的二叉树
            Node3 leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node3 rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有data,只有权值!(哈夫曼数中只有叶子节点有数据,飞叶子节点没有数据)
            Node3 parent = new Node3(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理过的两颗二叉树移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入
            nodes.add(parent);
        }
        //返回最后的节点就是哈夫曼树的根节点
        return nodes.get(0);

    }

    //前序遍历
    private static void preOrder(Node3 root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("哈夫曼树为空,不能遍历");
        }
    }

    //为了调用方便,重载getCodes
    private static Map<Byte,String> getCodes(Node3 root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left,"0",stringBuilder);
        //处理root的右子树
        getCodes(root.right,"1",stringBuilder);
        return huffmanCodes;
    }

    //生成哈夫曼树对应的哈夫曼编码
    //1.将哈夫曼编码表存放在Map<Byte,String>形式 32->01 97->100 100->11000....
    static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();
    //2.在生成哈夫曼编码表时,需要去拼接路径,定义一个StringBuilder存储某个叶子节点的路径
    static StringBuilder stringBuilder = new StringBuilder();
    /**
     * 功能:将传入的node节点的所有叶子节点的哈夫曼编码得到,并放入到哈夫曼集合中
     * @param node 传入节点
     * @param code 路径:左子节点是0,右子节点是1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(Node3 node,String code,StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//node为空时不处理
            //判断当前node是叶子节点还是非叶子节点
            if (node.data == null) {//非叶子节点
                //递归处理
                //向左递归
                getCodes(node.left,"0",stringBuilder2);
                //向右递归
                getCodes(node.right,"1",stringBuilder2);
            } else {//叶子节点
                //就表示找到了某个叶子节点的最后
                huffmanCodes.put(node.data,stringBuilder2.toString());
            }
        }
    }

    //将字符串对应的byte[]数组,通过生成的哈夫曼编码表,返回一个哈夫曼编码压缩后的byte[]

    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @param huffmanCodes 生成的哈夫曼编码表map
     * @return 返回哈夫曼编码处理后的对应的字节数组
     */
    private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes) {
        //1.先利用哈夫曼编码表将传进来的原始字节数组先转成哈夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历byte数组
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        System.out.println("哈夫曼编码字符串"+stringBuilder);//133

        //将stringBuilder转成byte[]
        //统计返回byte[] huffmanCodeBytes长度
        //一句话搞定:int len = (stringBuilder.length() + 7) / 8;
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }

        //创建存储压缩后的byte数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;//定义一个计数器,记录是第几个byte
        //遍历stringBuilder,根据它八位八位的放
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            //因为是每8位对应一个byte
            String strByte;
            if (i + 8 > stringBuilder.length()) {//最后,不够8位了
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            //将strByte转成一个byte,放入到huffmanCodeBytes
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

    //使用一个方法,将前面的方法封装起来,便于我们的调用
    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 经过哈夫曼编码处理后的字节数组(压缩后的数组)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        //1.将字节数组转换成节点数组
        List<Node3> nodes = getNodes(bytes);

        //2.将节点数组生成哈夫曼树
        Node3 huffmanTreeRoot = createHuffmanTree(nodes);//哈夫曼树的根节点

        //3.生成赫夫曼树对应的赫夫曼编码
        Map<Byte,String> huffmanCodes = getCodes(huffmanTreeRoot);//返回也行不返回也行

        //4.根据生成的哈夫曼编码,压缩得到压缩后的哈夫曼编码字节数组
        byte[] huffmanCodeByte = zip(bytes,huffmanCodes);//长度133

        return huffmanCodeByte;

    }

    //完成数据的解压
    //思路
    //1.将huffmanCodeBytes重新转成哈夫曼编码对应的二进制字符串
    //2.将哈夫曼编码对应的二进制字符串,对照着哈夫曼编码,转回到原始字符串

    /**
     *
     * @param flag 标识是否需要补高位,如果为真,表示需要补高位;如果为假,表示不需要补;如果是最后一个字节,无需补高位
     * @param b 传入的byte
     * @return 是b对应的二进制字符串(注意是按照补码返回的)
     */
    private static String byteToBitString(boolean flag,byte b) {
        //使用变量保存b
        int temp = b;//将b转成int

        //如果是正数我们还存在补高位
        if (flag) {
            temp |= 256;//按位与 temp 100000000|00000001=》100000001
        }

        String str = Integer.toBinaryString(temp);//返回的是temp对应的二进制补码
        //System.out.println(str);

        if (flag) {
            return str.substring(str.length() - 8);//取后面的八位
        } else {
            return str;
        }
    }

    //完成对压缩数据的解码

    /**
     *
     * @param huffmanCodes 哈夫曼编码表
     * @param huffmanBytes 经哈夫曼编码处理过的字符串得到的字节数组
     * @return 原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes) {

        //1.先得到huffmanBytes得到的二进制字符串
        StringBuilder stringBuilder = new StringBuilder();
        //将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag,b));
        }
//        System.out.println("哈夫曼字节数组对应的二进制字符串="+stringBuilder.toString());
//        return null;
        //把字符串按照指定的哈夫曼编码进行解码
        //把哈夫曼编码表进行调换,因为要反向查询
        Map<String,Byte> map = new HashMap<String, Byte>();
        for (Map.Entry<Byte,String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(),entry.getKey());
        }
        System.out.println(map);

        //创建一个集合存放byte
        List<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuilder.length(); ) {
            //i可以理解为一个索引,扫描
            int count = 1;//小的计数器
            boolean flag = true;
            Byte b= null;

            while (flag) {
                //取出一个'1''0'
                //递增的取出key
                String key = stringBuilder.substring(i,i + count);//i不动,让count动,指定匹配到一个字符
                b = map.get(key);
                if (b == null) {//说明没有匹配到
                    count++;
                } else {
                    //匹配到
                    flag = false;
                }
            }
            list.add(b);
            i += count;//i直接移动到count
        }
        //当for循环结束后,我们list中就存放了所有的字符
        //把list中的数据放到byte[]并返回
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

}


//创建节点,带数据和权值
class Node3 implements Comparable<Node3>{
    Byte data;//存放数据本身的ASCII码值,'a'->97 ' '->32
    int weight;//权值,表示出字符出现的次数
    Node3 left;
    Node3 right;

    public Node3(Byte data,int weight) {
        this.data = data;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node3 o) {
        return this.weight - o.weight;//从小到大排序
    }

    @Override
    public String toString() {
        return "Node3{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

最佳实践-文件压缩
我们学习了通过赫夫曼编码对一个字符串进行编码和解码, 下面我们来完成对文件的压缩和解压, 具体要求:给你一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。
思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩

代码实现:

最佳实践-文件解压(文件恢复)
具体要求:将前面压缩的文件,重新恢复成原来的文件。
思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)

代码实现:

package com.zcr.tree;

import java.io.*;
import java.util.*;

/**
 * @author zcr
 * @date 2019/7/8-20:49
 */
public class HuffmanCode {

    public static void main(String[] args) {

        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();//字符串转换成字节数组
        System.out.println(contentBytes.length);//40

        /*//分步骤过程
        //将字节数组转换成节点数组
        List<Node3> nodes = getNodes(contentBytes);
        System.out.println(nodes);

        //将节点数组生成哈夫曼树
        System.out.println("哈夫曼树:");
        Node3 huffmanTreeRoot = createHuffmanTree(nodes);//哈夫曼树的根节点

        //前序遍历哈夫曼树
        System.out.println("前序遍历哈夫曼树");
        preOrder(huffmanTreeRoot);

        //生成赫夫曼树对应的赫夫曼编码
//        getCodes(huffmanTreeRoot,"",stringBuilder);
        getCodes(huffmanTreeRoot);//返回也行不返回也行
        System.out.println("生成的哈夫曼编码表为:"+huffmanCodes);

        //生成字符串的哈夫曼编码
        byte[] huffmanCodeByte = zip(contentBytes,huffmanCodes);//长度133
        System.out.println("压缩后的字符串为:"+Arrays.toString(huffmanCodeByte));//17
*/

        byte[] huffmanCodeBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果是"+Arrays.toString(huffmanCodeBytes));

        byte[] sourceBytes = decode(huffmanCodes,huffmanCodeBytes);
        System.out.println("原来的字符串是"+new String(sourceBytes));

        //压缩文件
        String srcFile = "F://we.jpg";
        String dsrFile = "F://we.zip";

        zipFile(srcFile,dsrFile);
        System.out.println("压缩文件成功");

        //解压文件
        String dsrFile2 = "F://we.zip";
        String srcFile2 = "F://we.jpg";

        unZipFile(srcFile2,dsrFile2);
        System.out.println("解压文件成功");



    }

    /**
     * @param bytes 接收一个字节数组(字符串-》字节数组)
     * @return 返回的就是一个节点列表(Map【值,次数】-》节点列表【值,次数】)
     */
    private static List<Node3> getNodes(byte[] bytes) {
        //1.先创建一个ArrayList
        List<Node3> nodes = new ArrayList<Node3>();


        //2.遍历bytes,统计每一个byte出现的次数,使用map存储
        Map<Byte, Integer> counts = new HashMap<Byte, Integer>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {//map还没有这个字符数据
                counts.put(b, 1);
            } else {//map中已经有了这个字
                counts.put(b, count+1);
            }
        }

        //把每个键值对转成一个Node对象,并加入到nodes集合
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node3(entry.getKey(), entry.getValue()));
        }
        return nodes;

    }

    //通过节点列表创建对应的huffman树
    private static Node3 createHuffmanTree(List<Node3> nodes) {
        while (nodes.size() > 1) {
            //从小到大排序
            Collections.sort(nodes);
            System.out.println("nodes="+nodes);

            //取出第一颗最小的二叉树
            Node3 leftNode = nodes.get(0);
            //取出第二颗最小的二叉树
            Node3 rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有data,只有权值!(哈夫曼数中只有叶子节点有数据,飞叶子节点没有数据)
            Node3 parent = new Node3(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理过的两颗二叉树移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树加入
            nodes.add(parent);
        }
        //返回最后的节点就是哈夫曼树的根节点
        return nodes.get(0);

    }

    //前序遍历
    private static void preOrder(Node3 root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("哈夫曼树为空,不能遍历");
        }
    }

    //为了调用方便,重载getCodes
    private static Map<Byte,String> getCodes(Node3 root) {
        if (root == null) {
            return null;
        }
        //处理root的左子树
        getCodes(root.left,"0",stringBuilder);
        //处理root的右子树
        getCodes(root.right,"1",stringBuilder);
        return huffmanCodes;
    }

    //生成哈夫曼树对应的哈夫曼编码
    //1.将哈夫曼编码表存放在Map<Byte,String>形式 32->01 97->100 100->11000....
    static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();
    //2.在生成哈夫曼编码表时,需要去拼接路径,定义一个StringBuilder存储某个叶子节点的路径
    static StringBuilder stringBuilder = new StringBuilder();
    /**
     * 功能:将传入的node节点的所有叶子节点的哈夫曼编码得到,并放入到哈夫曼集合中
     * @param node 传入节点
     * @param code 路径:左子节点是0,右子节点是1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(Node3 node,String code,StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将code加入到stringBuilder2
        stringBuilder2.append(code);
        if (node != null) {//node为空时不处理
            //判断当前node是叶子节点还是非叶子节点
            if (node.data == null) {//非叶子节点
                //递归处理
                //向左递归
                getCodes(node.left,"0",stringBuilder2);
                //向右递归
                getCodes(node.right,"1",stringBuilder2);
            } else {//叶子节点
                //就表示找到了某个叶子节点的最后
                huffmanCodes.put(node.data,stringBuilder2.toString());
            }
        }
    }

    //将字符串对应的byte[]数组,通过生成的哈夫曼编码表,返回一个哈夫曼编码压缩后的byte[]

    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @param huffmanCodes 生成的哈夫曼编码表map
     * @return 返回哈夫曼编码处理后的对应的字节数组
     */
    private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes) {
        //1.先利用哈夫曼编码表将传进来的原始字节数组先转成哈夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历byte数组
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        System.out.println("哈夫曼编码字符串"+stringBuilder);//133

        //将stringBuilder转成byte[]
        //统计返回byte[] huffmanCodeBytes长度
        //一句话搞定:int len = (stringBuilder.length() + 7) / 8;
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }

        //创建存储压缩后的byte数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;//定义一个计数器,记录是第几个byte
        //遍历stringBuilder,根据它八位八位的放
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            //因为是每8位对应一个byte
            String strByte;
            if (i + 8 > stringBuilder.length()) {//最后,不够8位了
                strByte = stringBuilder.substring(i);
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            //将strByte转成一个byte,放入到huffmanCodeBytes
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

    //使用一个方法,将前面的方法封装起来,便于我们的调用
    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 经过哈夫曼编码处理后的字节数组(压缩后的数组)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        //1.将字节数组转换成节点数组
        List<Node3> nodes = getNodes(bytes);

        //2.将节点数组生成哈夫曼树
        Node3 huffmanTreeRoot = createHuffmanTree(nodes);//哈夫曼树的根节点

        //3.生成赫夫曼树对应的赫夫曼编码
        Map<Byte,String> huffmanCodes = getCodes(huffmanTreeRoot);//返回也行不返回也行

        //4.根据生成的哈夫曼编码,压缩得到压缩后的哈夫曼编码字节数组
        byte[] huffmanCodeByte = zip(bytes,huffmanCodes);//长度133

        return huffmanCodeByte;

    }

    //完成数据的解压
    //思路
    //1.将huffmanCodeBytes重新转成哈夫曼编码对应的二进制字符串
    //2.将哈夫曼编码对应的二进制字符串,对照着哈夫曼编码,转回到原始字符串

    /**
     *
     * @param flag 标识是否需要补高位,如果为真,表示需要补高位;如果为假,表示不需要补;如果是最后一个字节,无需补高位
     * @param b 传入的byte
     * @return 是b对应的二进制字符串(注意是按照补码返回的)
     */
    private static String byteToBitString(boolean flag,byte b) {
        //使用变量保存b
        int temp = b;//将b转成int

        //如果是正数我们还存在补高位
        if (flag) {
            temp |= 256;//按位与 temp 100000000|00000001=》100000001
        }

        String str = Integer.toBinaryString(temp);//返回的是temp对应的二进制补码
        //System.out.println(str);

        if (flag) {
            return str.substring(str.length() - 8);//取后面的八位
        } else {
            return str;
        }
    }

    //完成对压缩数据的解码

    /**
     *
     * @param huffmanCodes 哈夫曼编码表
     * @param huffmanBytes 经哈夫曼编码处理过的字符串得到的字节数组
     * @return 原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes) {

        //1.先得到huffmanBytes得到的二进制字符串
        StringBuilder stringBuilder = new StringBuilder();
        //将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag,b));
        }
//        System.out.println("哈夫曼字节数组对应的二进制字符串="+stringBuilder.toString());
//        return null;
        //把字符串按照指定的哈夫曼编码进行解码
        //把哈夫曼编码表进行调换,因为要反向查询
        Map<String,Byte> map = new HashMap<String, Byte>();
        for (Map.Entry<Byte,String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(),entry.getKey());
        }
        System.out.println(map);

        //创建一个集合存放byte
        List<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuilder.length(); ) {
            //i可以理解为一个索引,扫描
            int count = 1;//小的计数器
            boolean flag = true;
            Byte b= null;

            while (flag) {
                //取出一个'1''0'
                //递增的取出key
                String key = stringBuilder.substring(i,i + count);//i不动,让count动,指定匹配到一个字符
                b = map.get(key);
                if (b == null) {//说明没有匹配到
                    count++;
                } else {
                    //匹配到
                    flag = false;
                }
            }
            list.add(b);
            i += count;//i直接移动到count
        }
        //当for循环结束后,我们list中就存放了所有的字符
        //把list中的数据放到byte[]并返回
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    //将一个文件进行压缩

    /**
     *
     * @param srcFile 你传入的希望压缩的文件的全路径
     * @param dstFile 我们压缩后将压缩文件放到哪个目录下
     */
    public static void zipFile(String srcFile,String dstFile) {
        //创建输出流
        FileInputStream is = null;
        OutputStream os = null;
        ObjectOutputStream oos = null;
        try {
            //创建文件的输入流,准备读取文件
            is = new FileInputStream(srcFile);
            //创建一个和源文件大小一样的byte[
            byte[] b = new byte[is.available()];
            //读取文件
            is.read();

            //直接对源文件压缩
            byte[] huffmanBytes = huffmanZip(b);

            //创建文件的输出流,准备存放压缩文件
            os = new FileOutputStream(dstFile);
            //创建一个和文件输出流相关联的ObjectOutputStream
            oos = new ObjectOutputStream(os);
            //把哈夫曼编码后的字节数组写入压缩文件
            oos.writeObject(huffmanBytes);
            //这里以对象流的方式写入哈夫曼编码,目的是为了以后好恢复
            //一定要把哈夫曼编码写入到压缩文件中,否则以后恢复不了
            oos.writeObject(huffmanCodes);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                is.close();
                os.close();
                oos.close();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }

    //完成对压缩文件的解压

    /**
     *
     * @param zipFile 准备解压的文件
     * @param dstFile 将文件解压到哪个路径
     */
    public static void unZipFile(String zipFile,String dstFile) {
        //定义文件输入流
        InputStream is = null;
        //定义一个对象输入流
        ObjectInputStream ois= null;
        //定义文件的输出流
        OutputStream os = null;



        try {
            //创建文件输入流
            is= new FileInputStream(zipFile);
            //创建一个和is关联的对象输入流
            ois = new ObjectInputStream(is);

            //读取byte数组
            byte[] huffmanBytes = (byte[])ois.readObject();
            //读取哈夫曼编码表
            Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();
            //解码
            byte[] bytes = decode(huffmanCodes,huffmanBytes);
            //将bytes写入到目标文件
            os = new FileOutputStream(dstFile);
            //写数据到目标文件
            os.write(bytes);


        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                ois.close();
                os.close();
                is.close();
            } catch (Exception e2) {
                System.out.println(e2.getMessage());
            }

        }


    }


}


//创建节点,带数据和权值
class Node3 implements Comparable<Node3>{
    Byte data;//存放数据本身的ASCII码值,'a'->97 ' '->32
    int weight;//权值,表示出字符出现的次数
    Node3 left;
    Node3 right;

    public Node3(Byte data,int weight) {
        this.data = data;
        this.weight = weight;
    }


    @Override
    public int compareTo(Node3 o) {
        return this.weight - o.weight;//从小到大排序
    }

    @Override
    public String toString() {
        return "Node3{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    //前序遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}

赫夫曼编码压缩文件注意事项

如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件 [举例压一个 .ppt]
赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml文件]
如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显.

.

二叉排序树

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

解决方案分析
使用数组
数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢. [示意图]
数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。[示意图]

使用链式存储-链表不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。[示意图]

使用二叉排序树

二叉排序树介绍
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
在这里插入图片描述
二叉排序树创建和遍历
一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为
代码实现(看老师演示)

package com.zcr.tree;

/**
 * @author zcr
 * @date 2019/7/9-11:51
 */
public class BinarySortTreeDemo {

    public static void main(String[] args) {

        int[] arr = {7,3,10,12,5,1,9};
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环的添加节点到二叉排序树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node4(arr[i]));
        }

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

    }
}

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

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

//创建节点
class Node4 {
    int value;
    Node4 left;
    Node4 right;

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

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

    //添加节点
    //递归的形式添加节点,注意需要满足二叉排序树的要求
    public void add(Node4 node) {
        if (node == null) {
            return;
        }

        //判断传入节点的值,和当前子树的根节点的值的关系
        if (node.value < this.value) {
            if (this.left == null) {//如果当前节点左子树为空
                this.left = node;
            } else {
                this.left.add(node);//递归的向左子树添加
            }
        } else if (node.value > this.value) {
            if (this.right == null) {
                this.right = node;
            } else {
                this.right.add(node);
            }
        }

    }

    //中序遍历
    public void infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }
}

二叉排序树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
删除叶子节点 (比如:2, 5, 9, 12)
删除只有一颗子树的节点 (比如:1)
删除有两颗子树的节点. (比如:7, 3,10 )
[示意图]
在这里插入图片描述
二叉排序树的删除
删除叶子节点删除的节点是叶子节点,即该节点下没有左右子节点。比如这里的 (比如:2, 5, 9, 12)
思路分析
代码实现
在这里插入图片描述
二叉排序树的删除
删除节点有一个子节点删除的节点有一个子节点,即该节点有左子节点或者右子节点。比如这里的 (比如:1 )
思路分析
代码实现
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二叉排序树的删除
删除节点有两个子节点删除的节点有两个子节点,即该节点有左子节点和右子节点。比如这里的 (比如:7,3,10)
思路分析
代码实现
课后练习:完成老师代码,并使用第二种方式来解决.
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
比如说:现在多了一个节点11
在这里插入图片描述
在这里插入图片描述

package com.zcr.tree;

/**
 * @author zcr
 * @date 2019/7/9-11:51
 */
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 Node4(arr[i]));
        }

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

        /*//删除叶子节点
        binarySortTree.delNode(2);
        System.out.println("删除叶子节点2后:");
        binarySortTree.infixOrder();*/

        /*//删除只有一颗子树的节点
        binarySortTree.delNode(1);
        System.out.println("删除叶子节点1后:");
        binarySortTree.infixOrder();*/

        //删除有两颗子树的节点
        binarySortTree.delNode(7);
        System.out.println("删除叶子节点7后:");
        binarySortTree.infixOrder();




    }
}

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

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

    //查找要删除的节点
    public Node4 search(int value) {
        if (root == null) {
            return null;
        } else {
            return root.search(value);
        }
    }

    //查找父节点
    public Node4 searchParent(int value) {
        if (root == null) {
            return null;
        } else {
            return root.searchParent(value);
        }
    }

    //删除节点
    public void delNode(int value) {
        if (root == null) {
            return;
        } else {
            //1.需要先去找到要删除的节点
            Node4 targetNode = search(value);
            //如果没有找到要删除的节点
            if (targetNode == null) {
                return;
            }
            //如果我们发现当前这颗二叉排序树只有一个节点
            if (root.left == null && root.right == null) {
                root = null;
                return;
            }

            //去找到targetNode的父节点
            Node4 parent = searchParent(value);
            if (targetNode.left == null && targetNode.right == null) {//如果要删除的节点是叶子节点
                //判断targetNode是父节点的左子节点还是右子节点
                if (parent.left != null && parent.left.value == value) {
                    //说明targetNode是父节点的左子节点
                    parent.left = null;
                } else if (parent.right != null && parent.right.value == value) {
                    //说明targetNode是父节点的右子节点
                    parent.right = null;
                }
            } else if (targetNode.left != null && targetNode.right != null) {//删除有两颗子树的节点
                int minVal = delRightTreeMin(targetNode.right);
                targetNode.value = minVal;

                //也可以从左子树找最大的
            } else {//删除只有一颗子树的节点
                if (targetNode.left != null) {//如果要删除的节点有左子节点
                    if (parent.left.value == value) {//如果targetNode是parent的左子节点
                        parent.left = targetNode.left;
                    } else {//如果targetNode是parent的右子节点
                        parent.right = targetNode.left;
                    }
                } else {//如果要删除的节点有右子节点
                    if (parent.left.value == value) {//如果targetNode是parent的左子节点
                        parent.left = targetNode.right;
                    } else {//如果targetNode是parent的右子节点
                        parent.right = targetNode.right;
                    }
                }
            }
        }
    }

    /**
     * 同时还要删除以node为根节点的二叉排序树的最小节点
     * @param node 传入的节点(当做二叉排序树的根节点)
     * @return 返回的以node为根节点的二叉排序树的最小节点的值
     */
    public int delRightTreeMin(Node4 node) {
        Node4 target = node;
        //循环的查找左子节点,就会找到最小值
        while (target.left != null) {
            target = target.left;
        }
        //这时候target就指向了最小节点
        //删除最小节点
        delNode(target.value);
        return target.value;


    }
}

//创建节点
class Node4 {
    int value;
    Node4 left;
    Node4 right;

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

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

    //添加节点
    //递归的形式添加节点,注意需要满足二叉排序树的要求
    public void add(Node4 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 infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }

    //查找要删除的节点

    /**
     *
     * @param value 希望删除的节点的值
     * @return 如果找到返回该节点,否则返回null
     */
    public Node4 search(int value) {
        if (value == this.value) {//找到就是该节点
            return this;
        } else if (value < this.value) {//如果查找的值小于当前节点,向左子树递归查找
            if (this.left == null) {//如果左子节点为空,就不能再找了
                return null;
            }
            return this.left.search(value);
        } else {
            if (this.right == null) {//如果右子节点为空,就不能再找了
                return null;
            }
            return this.right.search(value);
        }
    }

    //查找要删除节点的父节点

    /**
     *
     * @param value 要找的节点的值
     * @return 返回的是要删除的节点的父节点,如果没有就返回null
     */
    public Node4 searchParent(int value) {
        //如果当前节点就是要删除的节点的父节点,就返回
        if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
            return this;
        } else {
            //如果查找的值小于当前节点的值,并且当前节点的左子节点不为空
            //递归的向左查找
            if (value < this.value && this.left != null) {
                return this.left.searchParent(value);
            } else if (value >= this.value && this.right != null) {
                return this.right.searchParent(value);
            } else {
                return null;//没有找到父节点(要删除的节点是根节点)
            }
        }
    }
}

在这里插入图片描述
有一个小问题修正一下

} else {//删除只有一颗子树的节点
                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 {//如果要删除的节点有右子节点
                    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;
                    }
                }
            }

平衡二叉树(AVL树)

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

左边BST 存在的问题分析:
左子树全部为空,从形式上看,更像一个单链表.
插入速度没有影响
查询速度明显降低(因为需要依次比较), 不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
解决方案-平衡二叉树(AVL)

基本介绍
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
举例说明, 看看下面哪些AVL树, 为什么?
在这里插入图片描述
平衡二叉树的前提是二叉排序树!!它是在二叉排序树的基础上形成的

应用案例-单旋转(左旋转)
要求: 给你一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}
思路分析(示意图)
代码实现

当右子树的高度比左子树的高度要高时,进行左旋转!目的:降低右子树的高度!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用案例-单旋转(右旋转)
要求: 给你一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9, 7, 6}
思路分析(示意图)
代码实现
当左子树的高度大于右子树的高度时,目的:降低左子树的高度
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用案例-双旋转
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列
int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL树.
int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL树
问题分析
解决思路分析
代码实现
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.zcr.tree;

/**
 * @author zcr
 * @date 2019/7/9-17:46
 */
public class AVLTreeDemo {

    public static void main(String[] args) {

        //int[] arr = {4,3,6,5,7,8};//测试左旋转
        //int[] arr = {10,12,8,9,7,6};//测试右旋转
        int[] arr = {10,11,7,6,8,9};//测试双旋转

        //创建一颗AVL树
        AVLTree avlTree = new AVLTree();
        //添加节点
        for (int i = 0; i < arr.length; i++) {
            avlTree.add(new Node5(arr[i]));
        }

        //中序遍历AVL树
        System.out.println("中序遍历AVL树");
        avlTree.infixOrder();

        //求出AVL树的高度
        System.out.println("在平衡处理后情况:");
        System.out.println("树的高度="+avlTree.getRoot().height());//4
        System.out.println("树的左子树的高度="+avlTree.getRoot().leftHeight());//1->2
        System.out.println("树的右子树的高度="+avlTree.getRoot().rightHeight());//3->2
        System.out.println("当前的根节点为="+avlTree.getRoot());

        //

    }
}

//创建二叉平衡树
class AVLTree {
    private Node5 root;

    public Node5 getRoot() {
        return root;
    }

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

    //添加节点的方法
    public void add(Node5 node) {
        if (root == null) {
            root = node;
        } else {
            root.add(node);
        }
    }

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

//创建节点
class Node5 {
    int value;
    Node5 left;
    Node5 right;

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

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

    //添加节点
    //递归的形式添加节点,注意需要满足二叉排序树的要求
    public void add(Node5 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);
            }
        }

        //当添加完一个节点后,如果右子树的高度-左子树的高度 > 1,就左旋转
        if (rightHeight() - leftHeight() > 1) {
            //如果它的右子树的左子树的高度大于它的右子树的右子树的高度
            if (right != null && right.rightHeight() < right.leftHeight()) {
                //先对右子树进行右旋转
                right.rightRotate();
                //再对当前节点进行左旋转
                leftRotate();
            } else {
                leftRotate();//直接左旋转即可
            }
            return;
        }

        //当添加完一个节点后,如果左子树的高度-右子树的高度 > 1,就左旋转
        if (leftHeight() - rightHeight() > 1) {
            //如果它的左子树的右子树的高度大于它左子树的高度
            if (left != null && left.rightHeight() > left.leftHeight()) {
                //先对当前节点的左子树进行左旋转
                left.leftRotate();
                //再对当前节点进行右旋转
                rightRotate();
            } else {
                rightRotate();//直接进行右旋转即可
            }
            return;
        }

    }

    //中序遍历
    public void infixOrder() {
        if (this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null) {
            this.right.infixOrder();
        }
    }

    //查找要删除的节点

    /**
     *
     * @param value 希望删除的节点的值
     * @return 如果找到返回该节点,否则返回null
     */
    public Node5 search(int value) {
        if (value == this.value) {//找到就是该节点
            return this;
        } else if (value < this.value) {//如果查找的值小于当前节点,向左子树递归查找
            if (this.left == null) {//如果左子节点为空,就不能再找了
                return null;
            }
            return this.left.search(value);
        } else {
            if (this.right == null) {//如果右子节点为空,就不能再找了
                return null;
            }
            return this.right.search(value);
        }
    }

    //查找要删除节点的父节点

    /**
     *
     * @param value 要找的节点的值
     * @return 返回的是要删除的节点的父节点,如果没有就返回null
     */
    public Node5 searchParent(int value) {
        //如果当前节点就是要删除的节点的父节点,就返回
        if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
            return this;
        } else {
            //如果查找的值小于当前节点的值,并且当前节点的左子节点不为空
            //递归的向左查找
            if (value < this.value && this.left != null) {
                return this.left.searchParent(value);
            } else if (value >= this.value && this.right != null) {
                return this.right.searchParent(value);
            } else {
                return null;//没有找到父节点(要删除的节点是根节点)
            }
        }
    }

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

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

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

    //左旋转
    private void leftRotate() {
        //创建新的节点,以当前根节点的值
        Node5 newNode = new Node5(value);
        //把新的节点的左子树,设置为,当前节点的左子树
        newNode.left = left;
        //把新的节点的右子树,设置为,当前节点的右子树的左子树
        newNode.right = right.left;
        //把当前节点的值,替换成,当前节点的右子节点的值
        value = right.value;
        //把当前节点的右子树,设置为,当前节点右子树的右子树
        right = right.right;
        //把当前节点的左子树,设置为,新的节点
        left = newNode;
    }

    //右旋转
    private void rightRotate() {
        //创建新的节点,以当前根节点的值
        Node5 newNode = new Node5(value);
        //把新的节点的右子树,设置为,当前节点的右子树
        newNode.right = right;
        //把新的节点的左子树,设置为,当前节点的左子树的右子树
        newNode.left = left.right;
        //把当前节点的值,替换成,当前节点的左子节点的值
        value = left.value;
        //把当前节点的左子树,设置为,当前节点左子树的左子树
        left = left.left;
        //把当前节点的右子树,设置为,新的节点
        right = newNode;
    }


}

多路查找树

二叉树与B树
二叉树的问题分析
二叉树的操作效率较高,但是也存在问题, 请看下面的二叉树
在这里插入图片描述
二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如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树结构, 具有如下特点:
1.2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
2.有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
3.有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点.

4.2-3树是由二节点和三节点构成的树。
在这里插入图片描述

2-3树应用案例
将数列{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20} 构建成2-3树,并保证数据插入的大小顺序【满足排序树的特点】。(演示一下构建2-3树的过程.)
12个数据
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
插入规则:
1.2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
2.有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
3.有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
4.当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面3个条件。
5.对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
在这里插入图片描述

其它说明
除了23树,还有234树等,概念和23树类似,也是一种B树。 如图:
二节点
三节点四节点

在这里插入图片描述

B树、B+树和B*树

B树的介绍
B-tree树即B树,B即Balanced,平衡的意思。有人把B-tree翻译成B-树,容易让人产生误解。会以为B-树是一种树,而B树又是另一种树。实际上,B-tree就是指的B树。

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

B+树的介绍
B+树是B树的变体,也是一种多路搜索树。
在这里插入图片描述
在这里插入图片描述

B树的介绍
B
树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
在这里插入图片描述
在这里插入图片描述

6.2树的定义 150

树的定义其实就是我们在讲解栈时提到的递归的方法。也就是在树的定义之中还用到了树的概念,这是比较新的一种定义方法。

树(Tree)是n(n≥0)个结点的有限集合。n=0时称为空树,在任意一棵非空树中:
(1)有且仅有一个特定的称为根(Root)的结点
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2…Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)

在这里插入图片描述
对于树的定义还需要强调两点:

  1. n>0 时根结点是唯一的,不可能存在多个根结点,别和现实中的大树混在一 起,现实中的树有很多根须,那是真实的树,数据结向中的树是只能有一个根 结点。
  2. m>0 时,子树的个数没有限制,但它们一定是互不相交的。像图 6-2-3 中的两 个结构就不符合树的定义,因为芭们都有相交的子树。

在这里插入图片描述

6.2.1结点分类 152

树的结点包含一个数据元素及若干指向其子树的分支。

结点拥有的子树数称为结点的度(Degree),度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。

除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。

因为这棵树结点的度的最大值是结点 D 的 度,为 3,所以树的度也为 3。

在这里插入图片描述

6.2.2结点间关系 152

结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。

同一个双亲的孩子之间互称兄弟(Sibling)。

结点的祖先是从根到该结点所经分支上的所有结点。所以对于 H 来说, D、 B、 A 都是 宫的祖先。 反之, 以某结点为根的子树中的任一结点都称为该结点的子孙。 B 的子孙有 D、 G、 H、 I,如图
在这里插入图片描述

6.2.3树的其他相关概念 153

结点层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第i层,则其子树的根就在第i+1层。

其双亲在同一层的结点直为堂兄弟。显然 图中的 D、 E、 F 是堂兄弟,而 G、 H、 l、 J 也是。 树中结点的最大层次称为树 的深度 (Dep也)或高度,当前树的深度为 4。
在这里插入图片描述
在同一层的结点互为兄弟

如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。

森林(Forest)是m(m≥0)棵互不相交的树的集合。对树中每个结点而言,其 子树的集合即为森林。

线性结构与树结构对比

线性结构
第一个数据元素:无前驱
最后一个数据元素:无后继
中间元素:一个前驱一个后继

树结构
根结点:无双亲,唯一
叶结点:无孩子,可以多个
中间结点:一个双亲多个孩子

6.3树的抽象数据类型 154

在这里插入图片描述

6.4树的存储结构 155

先来看看顺序存储结构,用一段地址连续的存储单元依次存储线性表的数据元 素。这对于统性表来说是很自然的,对于树这样一多对的结构呢?

树中某个结点的孩子可以有多个,这就意味着, 无论按何种顺序将树中所有结点 存储到数组中,结点的存储位置都无法直接反映逻辑关系,你想想看,数据元素挨个 的存储,谁是谁的双亲,谁是谁的孩子呢?简单的顺序存储结构是不能满足树的实现 要求的。

不过充分利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的 表示。我们这里要介绍三种不同的表示法:双亲表示法、孩子表示法、孩子兄弟表 示法。

6.4.1双亲表示法 155

在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自己是谁以外,还知道 它的双亲在哪里。
在这里插入图片描述
其中 data 是数据域,存储结点的数据信息。而 parent 是指针域,存储该结点的双 亲在数组中的下标。

以下是我们的双亲表示法的结点结构定义代码。
在这里插入图片描述
在这里插入图片描述
由于根结点是没有双亲 的,所以我们约定根结点的位置域设置为-1 ,这也就意味着,我们所有的结点都存有它双亲的位置。
在这里插入图片描述
在这里插入图片描述
该存储方式根据结点的parent指针很容易找到它的双亲结点,时间复杂度为O(1) ,直到 parent 为一1 时,表示找到了树结点的根。

缺点: 如果需要知道某个结点的所有孩子,需要遍历整棵树

改进一下:
我们增加一个结点最左边孩子的域,不妨叫它长子域,这样就可以很 容易得到结点的孩子。 如果没有孩子的结点,这个长子域就设置为-1 ,如表
在这里插入图片描述
对于有 0 个或 1 个孩子结点来说, 这样的结构是解决了要找结点孩子的问题了。 甚至是有 2 个孩子, 知道了长子是谁,另一个当然就是次子了。

另外一个问题场景,我们很关注各兄弟之间的关系,双亲表示法无法体现这样的 关系,那我们怎么办?嗯,可以增加一个右兄弟域来体现兄弟关系,也就是说,每一 个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则 赋值为一1,如表
在这里插入图片描述
但如果结点的孩子很多,超过了 2 个。 我们又关注结点的双亲、 又关注结点的孩 子、还关注结点的兄弟,而且对时间遍历要求还比较高,那么我们还可以把此结构扩展为有双亲域、长子域、 再有右兄弟域。 存储结构的设计是一个非常灵活的过程。一 个存储结构设计得是否合理,取决于基于该存储结构的运算是否适合、是否方便,时 间复杂度好不好等。注意也不是越多越好,有需要时再设计相应的结构。

6.4.2孩子表示法 158

把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如下图所示
在这里插入图片描述
为此,设计两种结点结构, 一个是孩子链表的孩子结点
在这里插入图片描述
其中 child 是数据域,用来存储某个结点在表头数组中的下标。 next 是指针域,用 来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点
在这里插入图片描述
其中 data 是数据域,存储某结点的数据信息。 firstchild 是头指针域, 存储该结点的孩子链表的头指针。

以下是我们的孩子表示法的结构定义代码。
在这里插入图片描述
在这里插入图片描述
这样的结构对于我们要查找某个结点的某个孩子,或者找某个结点的兄弟,只需 要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的,对头结点的数组 循环即可。

缺点: 如果需要知道某个结点的双亲,需要遍历整棵树

改进: 双亲孩子表示法
在这里插入图片描述

6.4.3孩子兄弟表示法 162

任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的,因此,可以设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟
在这里插入图片描述
其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地 址, rightsib 是指针域,存储该结点的右兄弟结点的存储地址。
结构定义代码如下。
在这里插入图片描述
在这里插入图片描述

这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过 fistchii:l 找到 此结点的长子,然后再通过长子结点的 rightsib 找到它的二弟,接着一直下去,直到 找到具体的孩子。当然,如果想找某个结点的双亲,这个表示法也是有做陷的,那怎 么办呢?呵呵,对,如果真的有必要,完全可以再增加一个 p盯ent 指针域来解决快速查找 双亲的问题, 这里就不再细谈了。

这个表示法的最大好处是它把一棵复杂的树变成了一棵二叉树
在这里插入图片描述
这样就可以充分利用二叉树的特性和算法来处理这棵树了 。

6.5二叉树的定义 163

苏东坡曾说:“人有悲欢离合,月有阴晴圆缺,此事古难全”。意思就是完美是理想,不完美才是人生。我们通常举的例子也都是左高右低、参差不齐的二叉树。那是否存在完美的二叉树呢?

二叉树(Binary Tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成
在这里插入图片描述

6.5.1二叉树特点 164

每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。
左子树和右子树是有顺序的,次序不能任意颠倒。
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。

二叉树具有五种基本形态:

  1. 空二叉树。
  2. 只有一个根结点。
  3. 根结点只有左子树。
    4 . 根结点只有右子树。
    5 . 根结点既有左子树又有右子树。

是有三个结点的二叉树,考虑一下,又有几种形态?
在这里插入图片描述

6.5.2特殊二叉树 166

斜树(左斜树、右斜树):所有的结点都只有左子 树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜 树。有人会想,这也能叫树呀, 与我们的统性表结构不是一样吗。对的,其实线性表 结构就可以理解为是树的一种极其特殊的表现形式。

满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在 同一层上,这样的二叉树称为满二叉树。
在这里插入图片描述
单是每个结点都存在左右子树,不能算是满二叉树,还必须要所有的叶子都在同 一层上,这就做到了整棵树的平衡。因此,满二叉树的特点有 :
(1) 叶子只能出现在最下一层。出现在其他层就不可能达成平衡。
(2) 非叶子结点的度一定是 2。否则就是"缺胳膊少腿" 了。
(3) 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。

完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树位置完全相同,则这棵二叉树称为完全二叉树
在这里插入图片描述
满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满二叉树

完全二叉树的所有结点与同样深度的满二叉树,它们按层序编号相同的结 点, 是一一对应的。这里有个关键词是按层序编号

完全二叉树的特点:
(1) 叶子结点只能出现在最下两层。
(2) 最下层的叶子一定集中在左部连续位置。
(3) 倒数二层,若有叶子结点,一定都在右部连续位置。
(4) 如果结点度为 1,则该结点只有左孩子,即不存在只有右子树的情况。
(5) 同样结点数的二叉树,完全二叉树的深度最小。

6.6二叉树的性质 169

在二叉树的第i层上至多有2(i−1)个结点(i≥1)
深度为k的二叉树至多有2k−1个结点(k≥1)
对任何一棵二叉树T,其叶子结点数=度为2的结点数+1
具有n个结点的完全二叉树的深度不大于log2nlog2n+1的最大整数
如果对一棵有n个结点的完全二叉树的结点按层序编号(每层从左到右),对任一结点i(1≤i≤n)有:
如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点i/2
如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1

在这里插入图片描述

6.6.1二叉树性质1 169

在这里插入图片描述

6.6.2二叉树性质2 169

在这里插入图片描述

6.6.3二叉树性质3 169

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.6.4二叉树性质4 170

在这里插入图片描述
在这里插入图片描述

6.6.5二叉树性质5 171

在这里插入图片描述
在这里插入图片描述

6.7二叉树的存储结构 172

6.7.1二叉树顺序存储结构 172

二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。
在这里插入图片描述
在这里插入图片描述

上图浅色代表不存在的结点,不存在的结点用^表示,会造成对存储空间的浪费,所以顺序存储结构一般只用于完全二叉树

6.7.2二叉链表 173

二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域
在这里插入图片描述
其中data是数据域lchild 和rchild都是指针域,分别存放指向左孩子和右孩子的指针。

以下是我们的二叉链袤的结点结构定义代码。

class BiTNode<E>{
    E data;
    BiTNode<E> lchild,rchild;    
    public BiTNode(E data) {
        this.data=data;
        this.lchild=null;
        this.rchild=null;
    }
}
 
public class BiTree<E> {
     
    private BiTNode<E> root;
 
    public BiTree() {
        root=null;
    }
 
        ...
}

在这里插入图片描述

6.8遍历二叉树 174

你人生的道路上,高考填志愿要面临哪个城市、哪所大学、具体专业等选择,由于选择方式的不同,遍历的次序就完全不同。

6.8.1二叉树遍历原理 174

二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

访问其实是要根据实际的需要来确定具体做什么,比如对每个结点进行相关计 算, 输出打印等,包算作是一个抽象操作。在这里我们可以简单地假定就是输出结点 的数据信息。
二叉树的遍历次序不同于线性结构,最多也就是从头至尾、循环、双向等简单的 遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个 被访问的结点面临着不同的选择。

6.8.2二叉树遍历方法 175

1.前序遍历 规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子 树, 再前序遍历右子树。如图 6-8-2 所示,遍历的顺序为: ABDGHCEIF。
在这里插入图片描述

  1. 中序遍历
    规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结 点) ,中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。 如图 6-8-3 所示, 遍历的顺序为 : GDHBAEICF。
    在这里插入图片描述
  2. 后序遍历
    规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左 右子树,最后是访问根结点。 如图 6品4 所示i 遍历的顺序为.: GHDBIEFCA o
    在这里插入图片描述
  3. 层序遍历
    规则是若树为空, 则空操作返回,否则从树的第一层,也就是根结点开始访问, 从上而下逐层遍历,在同一层中, 按从左到右的颇用才结点逐个访问。如图 6-8-5 所 示,遍历的顺序为: ABCDEFGHL.
    在这里插入图片描述

有同学会说,研究这么多遍历的方法干什么呢?
我们用图形的方式来表现树的结构,应该说是非常直观和容易理解,但是对于计 算机来说,它只有循环、判断等方式来处理,也就是说,它只会处理线性序列,而我 们刚才提到的四种遍历方法,其实都是在把树中的结点变成某种意义的线性序列,这 就给程序的实现带来了好处。
另外不同的遍历提供了对结点依次处理的不同方式,可以在遍历过程中对结点进行各种处理。

6.8.3前序遍历算法 178

先访问根结点,然后前序遍历左子树,再前序遍历右子树

二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归,而且极其 简洁明了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.8.4中序遍历算法 181

从根结点开始(并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.8.5后序遍历算法 184

从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点

6.8.6推导遍历结果 184

有一种题目为了考查你对二叉树遍历的掌握程度,是这样出题的。已知一棵二叉 树的前序遍历序列为 ABCDEF, 中序遍历序列为 CBAEDF,请问这棵二叉树的后序遍 历结果是多少?

对于这样的题目,如果真的完全理解了前中后序的原理,是不难的。

三种遍历都是从根结点开始,前序遍历是先打印再递归左和右。所以前序遍历序 列为 ABCDEF, 第一个字母是 A 被打印出来,就说明 A 是根结点的数据。再由中序遍历序列是 CBAEDF,可以知道 C 和 B 是 A 的左子树的结点, E、 D、 F 是A 的右子树的 结点,如图 6-8-21 所示。
在这里插入图片描述

然后我们看前序中的 C 和 B, 它的顺序是ABCDEF,是先打印 B 后打印 C,所以B 应该是 A 的左孩子,而 C 就只能是 B 的孩子,此时是左还是右孩子还不确定。再看中 序序列是 CBAEDF, C 是在 B 的前面打印,也就说明 C 是 B 的左孩子,否则就是右孩 子了,如图 6-8-22 所示。
在这里插入图片描述
再看前序中的 E、 D、 F,它的顺序是 ABCDEF,那就意味着 D 是 A 结点的右孩 子, E 和 F 是 D 的子孙,注意,它们中有一个不一定是孩子,还有可能是孙子的。 再 来看中序序列是 CBAEDF,由于 E 在 D 的左侧,而 F 在右侧,所以可以确定 E 是 D 的左孩子 , F 是 D 的右孩子。因此最终得到的二叉树是图 6-8-23 所示。
在这里插入图片描述
为了避免推导中的失误,你最好在心中递归遍历,检查一下这棵树的前序和中序 遍历序列是否与题目中的相同。
已经复原了二叉树,要获得它的后序遍历结果就是易如反掌,结果是 CBEFDA.

但其实,如果同学们足够熟练,不同画这棵二叉树,也可以得到后序的结果,因为刚才判断了 A 结点是根结点,那么它在后序序列中 , 一定是最后一个。刚才推导出 C 是 B 的左孩子,而 B 是 A 的左孩子,那就意味着后序序列的前两位一定是 CB。 同 样的办法也可以得到 EFD 这样的后序顺序,最终就自然的得到 CBEFDA 这样的序列, 不用在草稿上面树状图了。

反过来,如果我们的题目是这样: 二叉树的中序序列是 ABCDEFG,后序序列是 BDCAFGE ,求前序序列。
这次简单点,由后序的 BDCAPGE,得到 E 是根结点,因此前序首字母是 E。
于是根据中序序列分为两棵树 ABCD 和 町,由后序序列的 WH:AFGE, 知道 A 是 E 的左孩子,前序序列目前分析为 EAo
再由中序序列的 AIlι.IlEFG ,知道 BCD 是 A 结点的右子孙,再由后序序列的 JmCAFGE 知道 C 结点是A 结点的右孩子,前序序列目前分析得到 EAC。
中序序列 AIlC.Ð .EFG ,得到 8 是 C 的左孩子, D 是 C 的右孩子,所以前序序列目 前分析结果为 EACBD.
由后序序列 BDCAfGE, 得到 G 是 E 的右孩子,于是 F 就是 G 的孩子。如果你是 在考试时做这道题目,时间就是分数、名次、学历,那么你根本不需关心 F 是 G 的左 还是右孩子,前序遍历序列的最终结果就是 EACBDGF。
不过细细分析,根据中序序列 ABCDE,E豆,是可以得出 F 是 G 的左孩子。

从这里我们也得到两个二叉树遍历的性质。
• 己知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
• 已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
但要注意了,已知前序和后序遍历,是不能确定一棵二叉树的, 原因也很简单, 比如前序序列是 ABC,后序序列是 CBA。 我们可以确定 A 一定是根结点,但接下来, 我们无法知道,哪个结点是左子树,哪个是右子树。这棵树可能有如图 6-8-24 所示的
四种可能。
在这里插入图片描述

层序遍历
从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问

二叉树的遍历加层序遍历(非递归)

非递归实现,需要先创建一个栈,利用其特性来进行储存和输出。

前序遍历,先输出当前点的值,一直沿着左子树进行读取,没左子树就在右子树重复上述过程。
中序遍历与前序遍历基本一致,只是输出值的代码位置不同。
后序遍历由于要左右子树输出完后才能输出根结点,所以增加一个栈进行标记是否完成左右子树的输出,其余思想基本类似。

下面代码中,要注意node的结点位置和stack.peek()的位置关系。

此外,在后序非递归遍历的过程中,栈中保留的是当前结点的所有祖先。这是和先序及中序遍历不同的。在某些和祖先有关的算法中,此算法很有价值。

package BiTree;
 
import java.util.LinkedList;
import java.util.Stack;
 
class BiTNode<E>{
    E data;
    BiTNode<E> lchild,rchild;    
    public BiTNode(E data) {
        this.data=data;
        this.lchild=null;
        this.rchild=null;
    }
}
 
public class BiTree<E> {
     
    private BiTNode<E> root;
 
    public BiTree() {
        //root=new BiTNode(null, null, null);
        root=null;
    }
     
    /*
     * 前序遍历
     */
    public void preOrder() {
        preOrderTraverse(root);
        System.out.println();
    }  
    private void preOrderTraverse(BiTNode<E> node) {
        if(node==null)
            return;
        System.out.print(node.data);
        preOrderTraverse(node.lchild);
        preOrderTraverse(node.rchild);
    }
     
    /*
     * 中序遍历
     */
    public void inOrder() {
        inOrderTraverse(root);
        System.out.println();
    }  
    private void inOrderTraverse(BiTNode<E> node) {
        if(node==null)
            return;
        inOrderTraverse(node.lchild);
        System.out.print(node.data);       
        inOrderTraverse(node.rchild);
    }
     
    /*
     * 后序遍历
     */
    public void postOrder() {
        postOrderTraverse(root);
        System.out.println();
    }  
    private void postOrderTraverse(BiTNode<E> node) {
        if(node==null)
            return;
        postOrderTraverse(node.lchild);            
        postOrderTraverse(node.rchild);
        System.out.print(node.data);
    }
     
     
    //===============循环遍历===============
    /**
     * 前序遍历(非递归)
     */
    public void preOrder2() {
        preOrder2(root);
        System.out.println();
    }
    private void preOrder2(BiTNode node) {
        Stack<BiTNode> stack = new Stack<BiTNode>();
        while(node!=null||!stack.isEmpty()) {
            while(node!=null) {
                System.out.print(node.data);
                stack.push(node);
                node=node.lchild;
            }
            node=stack.pop().rchild;       
        }
    }
     
     
    /**
     * 中序遍历
     */
    public void inOrder2() {
        inOrder2(root);
        System.out.println();
    }
    private void inOrder2(BiTNode node) {
        Stack<BiTNode> stack = new Stack<BiTNode>();
        while(node!=null||!stack.isEmpty()) {
            while(node!=null) {
                stack.push(node);
                node=node.lchild;
            }                  
            node=stack.pop();
            System.out.print(node.data);
            node=node.rchild;
        }      
    }
     
     
    /**
     * 后序遍历
     */
    public void postOrder2() {
        postOrder2(root);
        System.out.println();
    }
    private void postOrder2(BiTNode node) {
        Stack<BiTNode> stack = new Stack<BiTNode>();
        Stack<Integer> tag = new Stack<Integer>(); 
//      while(node!=null||!stack.isEmpty()) {
//          while(node!=null){
//              stack.push(node);
//              tag.push(0);
//              node=node.lchild;
//          }
            //这里的tag用于标记当前结点是否完成左右子结点遍历(所以用0,1表示)
//          while(!tag.isEmpty()&&tag.peek()==1) {  //栈顶节点的左右子结点已完成遍历
//              System.out.print(stack.pop().data);
//              tag.pop();
//          }
//          if(!tag.isEmpty()) {   //上面和这里的 !flag.isEmpty() 不可省略,不然会出错。
//              tag.pop();
//              tag.push(1);
//              node=stack.peek().rchild;
//          }          
//      }
        /*后序遍历时,分别从左子树和右子树共两次返回根结点(用tag表示次数),
         * 只有从右子树返回时才访问根结点,所以增加一个栈标记到达结点的次序。
         */
        while(node!=null||!stack.isEmpty()) {
            if(node!=null){
                stack.push(node);
                tag.push(1);  //第一次访问
                node=node.lchild;
            }else {
                if(tag.peek()==2) {
                    System.out.print(stack.pop().data);
                     
                    tag.pop();
                }else {
                    tag.pop();
                    tag.push(2); //第二次访问
                    node=stack.peek().rchild;
                }          
            }      
        }
    }
     
     
    //=========层序遍历============
    public void levelOrder() {
        BiTNode<E> node =root;
        LinkedList<BiTNode<E>> list = new LinkedList<>();
        list.add(node);
        while(!list.isEmpty()) {
            node=list.poll();
            System.out.print(node.data);
            if(node.lchild!=null)
                list.offer(node.lchild);
            if(node.rchild!=null)
                list.offer(node.rchild);
        }
    }
         
     
    public static void main(String[] args) {
        BiTree<String> aBiTree = new BiTree<String>();
        aBiTree.root=new BiTNode<String>("A");
        aBiTree.root.lchild=new BiTNode<String>("B");
        aBiTree.root.rchild=new BiTNode<String>("C");
        aBiTree.root.lchild.rchild=new BiTNode<String>("D");
 
//      BiTree<String> aBiTree = new BiTree<String>();
//      aBiTree.root=new BiTNode("A");
//      aBiTree.root.lchild=new BiTNode("B");
//      aBiTree.root.lchild.lchild=new BiTNode("C");
//      aBiTree.root.lchild.lchild.lchild=new BiTNode("D");
//      aBiTree.root.lchild.rchild=new BiTNode("E");
//      aBiTree.root.lchild.rchild.lchild=new BiTNode("F");
//      aBiTree.root.lchild.rchild.lchild.rchild=new BiTNode("G");
//      aBiTree.root.lchild.rchild.lchild.rchild.rchild=new BiTNode("H");
         
        System.out.println("————前序————");
        aBiTree.preOrder();
        aBiTree.preOrder2();
        System.out.println("————中序————");
        aBiTree.inOrder();
        aBiTree.inOrder2();
        System.out.println("————后序————");
        aBiTree.postOrder();
        aBiTree.postOrder2();
        System.out.println("————层序遍历————");
        aBiTree.levelOrder();
    }      
}

————前序————
ABDC
ABDC
————中序————
BDAC
BDAC
————后序————
DBCA
DBCA
————层序遍历————
ABCD

6.9二叉树的建立 187

我们如何在内存中生成一棵二叉链表的二叉树呢?

了能让每个结点确认 是否有左右孩子,我们对它进行了扩展,变成图 6牛1 右图的样子,也就是将二叉树 中每个结点的空指针引出一个虚结点,其值为一特定值,比如 U#"。我们称这种处理 后的二叉树为原二叉树的扩展二叉树。扩展二叉树就可以做到一个遍历序列确定一棵 二叉树了。比如图 6-9-1 的前序遍历序列就为 AB#D##C##。
在这里插入图片描述
有了这样的准备,我们就可以来看看如何生成一棵二叉树了。假设二叉树的结点 均为一个字符,我们把刚才前序遍历序列 AB#D##C##用键盘挨个输入。实现的算法如下:
在这里插入图片描述
在这里插入图片描述
其实建立二叉树,也是利用了递归的原理。只不过在原来应该是打印结点的地 方,改成了生成结点、给结点赋值的操作而已。所以大家理解了前面的遍历的话,对 于这段代码就不难理解了。

当然,你完全也可以用中序或后序遍历的方式实现二叉树的建立,只不过代码里 生成结点和构造左右子树的代码顺序交换一下。另外,输入的字符也要做相应的更 改。比如图 6-9-1 的扩展二叉树的中序遍历字符串就应该为#B#D#A#C#,而后序字符 串应该为###DB##CA.

以下为测试代码遍历的总体测试代码:

package BiTree;
 
class BiTNode<E>{
    E data;
    BiTNode<E> lchild,rchild;    
    public BiTNode(E data) {
        this.data=data;
        this.lchild=null;
        this.rchild=null;
    }
}
 
public class BiTree<E> {
     
    private BiTNode<E> root;
 
    public BiTree() {
        //root=new BiTNode(null, null, null);
        root=null;
    }
     
    /*
     * 前序遍历
     */
    public void preOrder() {
        preOrderTraverse(root);
        System.out.println();
    }  
    private void preOrderTraverse(BiTNode<E> node) {
        if(node==null)
            return;
        System.out.print(node.data);
        preOrderTraverse(node.lchild);
        preOrderTraverse(node.rchild);
    }
     
    /*
     * 中序遍历
     */
    public void inOrder() {
        inOrderTraverse(root);
        System.out.println();
    }  
    private void inOrderTraverse(BiTNode<E> node) {
        if(node==null)
            return;
        inOrderTraverse(node.lchild);
        System.out.print(node.data);       
        inOrderTraverse(node.rchild);
    }
     
    /*
     * 后序遍历
     */
    public void postOrder() {
        postOrderTraverse(root);
        System.out.println();
    }  
    private void postOrderTraverse(BiTNode<E> node) {
        if(node==null)
            return;
        postOrderTraverse(node.lchild);            
        postOrderTraverse(node.rchild);
        System.out.print(node.data);
    }
     
    /*
     * 6.9 二叉树的建立暂时不会,略
     */
     
    public static void main(String[] args) {
        BiTree<String> aBiTree = new BiTree<String>();
        aBiTree.root=new BiTNode("A");
        aBiTree.root.lchild=new BiTNode("B");
        aBiTree.root.rchild=new BiTNode("C");
        aBiTree.root.lchild.rchild=new BiTNode("D");
        System.out.println("————前序————");
        aBiTree.preOrder();
        System.out.println("————中序————");
        aBiTree.inOrder();
        System.out.println("————后序————");
        aBiTree.postOrder();
    }          
}

结果
————前序————
ABDC
————中序————
BDAC
————后序————
DBCA

6.10线索二叉树 188

我们现在提倡节约型社会,一切都应该节约为本。对待我们的程序当然也不例外,能不浪费的时间或空间,都应该考虑节省。

6.10.1线索二叉树原理 188

在二叉链表上,只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱是谁,后继是谁,可以利用如下结构,存放指向结点在某种遍历次序下的前驱和后继结点的地址
在这里插入图片描述
这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树

ltag为0时指向该结点的左孩子,为1时指向该结点的前驱
rtag为0时指向该结点的右孩子,为1时指向该结点的后继

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们如何知道某一结点的 协iki 是指 向宫的左孩子还是指向前驱? rchiki 是指向右孩子还是指向后继?比如 E 结点的 Ichild 是指向它的左孩子 J, 而 rchiki 却是指向色的后继 A. 显然我们在决定 Ichiki 是指向 左孩子还是前驱, rchíld 是指向右孩子还是后继上是需要一个区分标志的。因此,我 们在每个结点再增设两个标志域 ltag 和 rtag,注意ltag 和 rtag 只是存放 0 或 1 数字 的布尔型变里,其占用的内存空间要小于像 lchild 和 rchild 的指针变量。

在这里插入图片描述

如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,就比较适合用线索二叉链表的存储结构

线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。 由于前驱 和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改窒指针的过程。

6.10.2线索二叉树结构实现 191

package BiThrTree;
 
/**
 * 线索二叉树
 * 包含二叉树的中序线索化及其遍历
 * @author Yongh
 *
 */
class BiThrNode<E>{
    E data;
    BiThrNode<E> lChild,rChild;
    boolean lTag,rTag; 
    public BiThrNode(E data) {
        this.data=data;
        //tag都先定义成左右孩子指针。
        lTag=false;  //其实把Tag定义为IsThread更好
        rTag=false;
        lChild=null;
        rChild=null;
    }
}
 
public class BiThrTree<T> {
    BiThrNode<T> root;
    boolean link=false,thread=true;
     
    public BiThrTree() {
        root=null;
    }
             
    /*
     * 中序线索化二叉树
     * 即:在遍历的时候找到空指针进行修改。
     */
    BiThrNode<T> pre; //线索化时记录的前一个结点
     
    public void inThreading() {
        inThreading(root);
    }
    private void inThreading(BiThrNode<T> p) {
        if(p != null) {
            inThreading(p.lChild);//递归左子树线索化
            if(p.lChild==null) {//没有左孩子//表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,贼 值给了 pre,所以可以将 pre 赋值给 p-lchild,并修改 p->LTag;=Thread (也就是定义 为 1) 以完成前驱结点的线索化。
                p.lTag=thread;//前驱线索
                p.lChild=pre;//左孩子指针指向前驱
            }
            if(pre!=null && pre.rChild==null) {  //pre!=null一定要加上   //前驱没有右孩子  //后继就要稍稍麻烦一些。因为此时 p 结点的后继还没有访问到,因此只能对它的 前驱结点 pre 的右指针 rchild做判断, if (lpre->rchild) 表示如果为空,则 p 就是 pre 的后继,于是 pre->rchild=p,并且设置 pre->RTag=Thread ,完成后继结点的线索化。

                pre.rTag=thread;//后继线索
                pre.rChild=p;//前驱右孩子指针指向后继(当前节点p)
            }
            pre=p;                               //别忘了在这个位置加上pre=p     //保持pre指向p的前驱//完成前驱和后继的判断后,别忘记将当前的结点 p 赋值给 pre ,以便于下一次使用

            inThreading(p.rChild);//递归右子树线索化
        }
    }
     
    /*
     * 中序遍历二叉线索链表表示的二叉树(按后继方式)
     * 书中添加了一个头结点,本程序中不含头结点
     * 思路:先找到最左子结点
     */
    public void inOrderTraverse() {
        BiThrNode<T> p = root;
        while(p!=null) {
            while(p.lTag==link)//当ltag=0时,循环到中序序列第一个节点
                p=p.lChild;    //找到最左子结点
            System.out.print(p.data);
            while(p.rTag==thread) {   //不是if
                p=p.rChild;
                System.out.print(p.data);
            }
            p=p.rChild;//p进至其右子树根
        }  
        System.out.println();
    }
     
    /*
     * 中序遍历方法二(按后继方式)
     * 参考别人的博客
     */
    public void inOrderTraverse2() {
        BiThrNode<T> node = root;
        while(node != null && node.lTag==link) {
            node = node.lChild;
        }
        while(node != null) {
            System.out.print(node.data + ", ");           
            if(node.rTag==thread) {//如果右指针是线索
                node = node.rChild;
 
            } else {    //如果右指针不是线索,找到右子树开始的节点
                node = node.rChild;
                while(node != null && node.lTag==link) {
                    node = node.lChild;
                }
            }
        }
        System.out.println();
    }
     
    public static void main(String[] args) {
        BiThrTree<String> aBiThrTree = new BiThrTree<String>();
        aBiThrTree.root=new BiThrNode<String>("A");                   //      A
        aBiThrTree.root.lChild=new BiThrNode<String>("B");            //    /   \
        aBiThrTree.root.lChild.lChild=new BiThrNode<String>("C");     //   B     D
        aBiThrTree.root.rChild=new BiThrNode<String>("D");            //  /     / \
        aBiThrTree.root.rChild.lChild=new BiThrNode<String>("E");     // C     E   F
        aBiThrTree.root.rChild.rChild=new BiThrNode<String>("F");
        aBiThrTree.inThreading();
        aBiThrTree.inOrderTraverse();
        aBiThrTree.inOrderTraverse2();
    }  
} 

结果
C, B, A, E, D, F,

有了线索二叉树后,我们对官进行遍历时发现,其实就等于是操作一个双向链表 结构。

和双向链表结构一样,在二叉树线索链表上添加一个头结点,如图 6-10-6 所示, 并令其 lchild 域的指针指向二叉树的根结点(图中的①) ,其 rchild域的指针指向中序 遍历时访问的最后一个结点(图中的②)。反之,令二叉树的中序序列中的第一个结点 中, lchild域指针和最后一个结点的 rchild域指针均指向头结点(圈中的③和④)。这 样定义的好处就是我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结 点起j眼前驱进行遍历。

在这里插入图片描述

在这里插入图片描述
从这段代码也可以看出 , 官等于是一个链表的扫描,所以时间复杂度为 O(叶。
由于它充分利用了空指针域的空间(这等于节省了空间) ,又保证了创建时的一次 遍历就可以终生受用前驱后继的信息(这意味着节省了时间)。 所以在实际问题中, 如 果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采 用线索二叉链袤的存储结构就是非常不错的选择。

线索二叉树原理及前序、中序线索化(Java版)(文中对线索二叉树的介绍和代码都比较清晰,且更加全面)。
https://blog.csdn.net/UncleMing5371/article/details/54176252

6.11树、森林与二叉树的转换 195

有个乡镇企业也买了同样的生产线,老板发现这个问题后找了个小工来说:你必须搞定,不然炒你鱿鱼。小工很快想出了办法:他在生产线旁边放了台风扇猛吹,空皂盒自然会被吹走。

树的孩子兄弟法可以将一棵树用二叉链表进行 存储,所以借助二叉链衰,树和二叉树可以相互进行转换。从物理结构来看,它们的 二叉链表也是相同的,只是解释不大一样而已。因此,只要我们设定一定的规则,用 二叉树来表示树,甚至表示森林都是可以的, 森林与二叉树也可以互相进行转换。

6.11.1树转换为二叉树 196

将树转换为二叉树的步骤如下:
加线,在所有兄弟结点之间加一条连线
去线,对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明,注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子

在这里插入图片描述

6.11.2森林转换为二叉树 197

森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作

步骤如下:

1.把每个树转换为二叉树
2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来,当所有的二叉树连接起来后就得到了由森林转换来的二叉树

在这里插入图片描述

6.11.3二叉树转换为树 197

二叉树转换为树是树转换为二叉树的逆过程

步骤如下:
1.加线,若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点…都作为此结点的孩子,将该结点与这些右孩子结点用线连接起来
2.去线,删除原二叉树中所有结点与其右孩子结点的连线
3.层次调整,使之结构层次分明
在这里插入图片描述

6.11.4二叉树转换为森林 199

判断一棵二叉树能够转换成一棵树还是森林,标准很简单,只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树,转换成森林的步骤如下:

1.从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除…,直到所有右孩子连线都删除为止,得到分离的二叉树
2.再将每棵分离后的二叉树转换为树即可
在这里插入图片描述

6.11.5树与森林的遍历 199

树的遍历分为两种方式:
一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树,如下图遍历结果为ABEFCDG
另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点,如下图遍历结果为EFBCGDA
在这里插入图片描述

森林的遍历也分为两种方式:
前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林,如下图遍历结果为ABCDEFGHJI
后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林,如下图遍历结果为BCDAFEJHIG

在这里插入图片描述

森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同

这也就告诉我们, 当 以二叉链表作树的存储结构时,树的先根遍历和后根遍历完 全可以借用二叉树的前序遍历和中序遍历的算法来实现。这其实也就证实,我们找到 了对树和森林这种复杂问题的简单解决办法。

6.12赫夫曼树及其应用 200

压缩而不出错是如何做到的呢?简单的说,就是把我们要压缩的文本进行重新编码,以达到减少不必要的空间的技术。压缩和解压缩技术就是基于赫夫曼的研究之上发展而来,我们应该记住他。

6.12.1赫夫曼树 200

在这里插入图片描述
在这里插入图片描述

那么 70 分以上大约占总敏 80%的成绩都需要经过 3 次以上的判断才可以得到结 果,这显然不合理。
有没有好一些的办法,仔细观察发现,中等成绩 (70-79 分之间)比例最高,其 次是良好成绩,不及格的所占比例最少。我们把图 6-12-2 这棵二叉树重新进行分配。 改成如图 6-12-3 的做法试试看。

在这里插入图片描述
从图中感觉,应该效率要高一些了,到底高多少呢。这样的二叉树又是如何设计 出来的呢?我们来看看赫夫曼大叔是如何说的吧。

6.12.2赫夫曼树定义与原理 203

从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度

树的路径长度就是从树根到每一结点的路径长度之和
在这里插入图片描述
的二叉树 a 中, 根结点到结点 D 的 路径长度就为 4,二叉树 b 中根结点到结点 D 的路径长度为 2。
二 叉树 a 的树路径长度就为 1+1+2+2+3+3+4叫=20。二叉树 b 的树路才告氏度就为 1+2+3+3+2+1+2+2=16。

如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积,树的带权路径长度为树中所有叶子结点的带权路径长度之和。假设有 n 个权值{W1W2.…,Wn},构造一棵有 n 个叶子结点的二叉树,每个叶子结点带 权 Wk,每个叶子的路径长度为 lk,我何通常记作,则其中带权路径长度 WPL 最小的 二叉树称做赫夫曼树。

带权路径长度WPL最小的二叉树称做赫夫曼树

二叉树a的 WPL = 5x1+15x2+40x3+30x4+10x4 = 315

二叉树b的 WPL = 5x3+15x3+40x2+30x2+10x2 = 220

这样的结果意味着什么呢?如果我们现在有 10000 个学生的百分制成绩需要计算 王级分制成绩,用二叉树 a 的判断方法,需要做 31500 次比较,而二叉树 b 的判断方 法,只需要 22000 次比较, 差不多少了三分之一量,在性能上提高不是一点点。

构造赫夫曼树的步骤:
1.先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即:A5,E10,B15,D30,C40
2.取头两个最小权值的结点作为一个新结点N1的两个子结点,相对较小的是左孩子。这里就是 A 为 矶 的左孩子, E 为 Nl 的右孩子
3.将N1替换A与E,插入有序序列中,保持从小到大排列,即N115,B15,D30,C40
4.重复步骤2,3,直到只含一棵树为止。 灿 的权值=15+15=30。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

此时构造出来的赫夫曼树的 WPL = 40x1+30x2+15x3+10x4+5x4 = 205

通过刚才的步骤,我们可以得出构造赫夫曼树的赫夫曼算法描述。

  1. 根据给定的 n 个权值{ Wl,W2,…,Wn }构成 n 棵二叉树的集合 F={ Tl,T2,…Tn}, 其中每棵二叉树 Ti 中只有一个带权为 Wi根结点,其左右子树均为空。
    2 . 在 F 中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且 置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
    3 . 在 F 中删除这两棵树,同时将新得到的二叉树加入 F 中。
  2. 重复 2 和 3 步骤,直到 F 只含一棵树为止。这棵树便是赫夫曼树。

6.12.3赫夫曼编码 205

目的是为了解决当年远距离通信(主要是电报)的数据传输的最优化问题。

比如我们有一段文字内容为 “BADCADFEED” 要网络传输给别人,显然用二进制 的数字 (0 和 1) 来表示是很自然的想法。我们现在这段文字只有六个字母 ABCDEF,那么我们可以用相应的二进制数据表示,如表
在这里插入图片描述
这样真正传输的数据就是编码后的 00100001101000001101100100011n , 对 方接收时可以按照 3 位一分来译码。如果一篇文章很长,这样的二进制串也将非常的 可怕。 而且事实上,不管是英文、中文或是其他语言,字母或汉字的出现频率是不相 同的,比如英语中的几个元音字母 “aeiou”,中文中的"的了有在"等汉字都是频 率极高。

假设六个字母的频率为 A 27 , B 8, C 15 , D 15 , E 30, F 5 ,合起来正好是 100%。 那就意味着,我们完全可以重新按照赫夫曼树来规划它们。

左图为构造赫夫曼树的过程的权值显示。 右图为将权值左分支改为 0, 右分支改为 1 后的赫天曼树。
在这里插入图片描述
此时,我们对这六个字母用其从树根到叶子所经过路径的 0 或 1 来编码,可以得
在这里插入图片描述
我们将文字内容为 “BADCADFEED” 再次编码,对比可以看到结果串变小了 。
• 原编码二进制串 : OOlOOOOll01000001110ll00l000日 (共 30 个字符)
• 新编码二进制率: l0010100l0l01001000llll00 (共 25 个字符)

也就是说,我们的数据被压缩了,节约了大约 17%的存储或传输成本。 随着字符的增加和多字符权重的不同,这种压缩会更加显出其优势。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

哈夫曼的编码

https://www.cnblogs.com/skywang12345/p/3706833.html

6.13总结回顾 208

开头我们提到了树的定义,讲到了递归在树定义中的应用.提到了如子树、结 点、度、叶子、分支结点、双亲、孩子、层次、深度、森林等诸多概念,这些都是需 要在理解的基础上去记忆的。

我们谈到了树的存储结构时,讲了双亲表示法、孩子表示法、孩子兄弟表示法等 不同的存储结构。

并由孩子兄弟表示法引出了我们这章中最重要一种树,二叉树.

二叉树每个结点最多两棵子树,有左右之分。提到了斜树,满二叉树、完全二叉 树等特殊二叉树的概念。

我们接着谈到它的各种性质,这些性质给我们研究二叉树带来了方便。

二叉树的存储结构由于其特殊性使得既可以用顺序存储结构又可以用链式存储结构表示。

遍历是二叉树最重要的一门学间,前序、中序、 后序以及层序遍历都是需要熟练 掌握的知识。 要让自己要学会用计算机的运行思维去模拟递归的实现,可以加深我们 对递归的理解。不过,并非二叉树遍历就一定要用到递归,只不过递归的实现比较优 雅而已。这点需要明确。

二叉树的建立自然也是可以通过递归来实现。

研究中也发现, 二叉链表有很多浪费的空指针可以利用,查找某个结点的前驱和 后继为什么非要每次遍历才可以得到,这就引出了如何构造一棵线索二叉树的问题。 线索二叉树给二叉树的结点查找和遍历带来了高效率。

树、森林看似复杂,其实官们都可以转化为简单的二叉树来处理,我们提供了 树、森林与二叉树的互相转换的办法,这样就使得面对树和森林的数据结构时,编码 实现成为了可能。

最后,我们提到了关于二叉树的一个应用,赫夫曼树和赫夫曼编码,对于带权路 径的二叉树做了详尽地讲述,让你初步理解数据压缩的原理,并明白其是如何做到无损编码和无错解码的。

6.14结尾语 209

人受伤时会流下泪水。树受伤时,天将再不会哭。希望我们的未来不要仅仅是钢筋水泥建造的高楼,也要有那郁郁葱葱的森林和草地,我们人类才可能与自然和谐共处。

第7章图 211

图:
图 < Gnph) 是由顶点的有穷非空集合和顶点之间边的集合组成, 通常表示 为: G <V. E>, 其中 . G 表示一个图 . V 是图 G 中顶点的集舍. E 是图 G 中边的 集合。

为什么要有图:
前面我们学了线性表和树
线性表局限于一个直接前驱和一个直接后继的关系
树也只能有一个直接前驱也就是父节点
当我们需要表示多对多的关系时, 这里我们就用到了图

图的举例说明
图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。 结点也可以称为顶点。如图:
在这里插入图片描述

顶点(vertex)
边(edge)
路径
无向图(右图)
在这里插入图片描述
在这里插入图片描述
有向图
带权图
在这里插入图片描述

图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)。

邻接矩阵

邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是的row和col表示的是1…n个点。
在这里插入图片描述
邻接表
邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失.
邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
在这里插入图片描述

要求: 代码实现如下图结构.
在这里插入图片描述
在这里插入图片描述

思路分析 (1) 存储顶点String 使用 ArrayList (2) 保存矩阵 int[][] edges
代码实现

package com.zcr.graph;

import java.util.ArrayList;
import java.util.Arrays;

/**
 * @author zcr
 * @date 2019/7/9-21:05
 */
public class Graph {

    public static void main(String[] args) {
        //测试图
        int n = 5;//节点的个数
        String Vertexs[] ={"A","B","C","D","E"};
        //创建图
        Graph graph = new Graph(n);
        //循环的添加顶点
        for (String vertex : Vertexs) {
            graph.insertVertex(vertex);
        }
        //添加边 A-B A-C B-C B-D B-E
        graph.insertEdge(0,1,1);
        graph.insertEdge(0,2,1);
        graph.insertEdge(1,2,1);
        graph.insertEdge(1,3,1);
        graph.insertEdge(1,4,1);
        //打印图矩阵
        graph.showGraph();


    }

    private ArrayList<String> vertexList;//存储顶点集合
    private int[][] edges;//存储图对应的邻接矩阵
    private int numOfEdges;//表示边的数目

    //构造器
    public Graph(int n) {//n顶点的个数
        //初始化矩阵和vertexList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);
    }

    //插入顶点
    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    //添加边
    /**
     *
     * @param v1 第一个顶点的下标,即是第几个顶点 "A"-"B" 0-1
     * @param v2 第二个顶点的下标
     * @param weight 值
     */
    public void insertEdge(int v1,int v2,int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }

    //返回节点的个数
    public int getNumOfVertex() {
        return vertexList.size();
    }

    //得到边的数目
    public int getNumOfEdges() {
        return numOfEdges;
    }

    //返回节点 i(下标)对应的数据 0->"A"
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    //返回v1和v2的权值
    public int getWeight(int v1,int v2) {
        return edges[v1][v2];
    }

    //显示图对应的矩阵
    public void showGraph() {
        for (int[] link : edges) {
            System.out.println(Arrays.toString(link));
        }
    }



}

图的深度优先遍历介绍

图遍历介绍

所谓图的遍历,即是对结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略:
(1)深度优先遍历 (2)广度优先遍历

深度优先遍历基本思想
图的深度优先搜索(Depth First Search) 。
1.深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
2.我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
3.显然,深度优先搜索是一个递归的过程

深度优先遍历算法步骤
1.访问初始结点v,并标记结点v为已访问。
2.查找结点v的第一个邻接结点w。
3.若w存在,则继续执行4,如果w不存在,则回到第1步,将从v的下一个结点继续。
4.若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123)。
5.查找结点v的w邻接结点的下一个邻接结点,转到步骤3。

看一个具体案例分析:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图的广度优先遍历
广度优先遍历基本思想

图的广度优先搜索(Broad First Search) 。
类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点

广度优先遍历算法步骤
1.访问初始结点v并标记结点v为已访问。
2.结点v入队列
3.当队列非空时,继续执行,否则算法结束。
4.出队列,取得队头结点u。
5.查找结点u的第一个邻接结点w。
6.若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤:
6.1 若结点w尚未被访问,则访问结点w并标记为已访问。
6.2 结点w入队列
6.3 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图的深度优先VS 广度优先
在这里插入图片描述
在这里插入图片描述
深度优先遍历顺序为 1->2->4->8->5->3->6->7
广度优先算法的遍历顺序为:1->2->3->4->5->6->7->8

package com.zcr.graph;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

/**
 * @author zcr
 * @date 2019/7/9-21:05
 */
public class Graph {

    public static void main(String[] args) {
        //测试图
        int n = 8;//节点的个数
        //String Vertexs[] ={"A","B","C","D","E"};
        String Vertexs[] ={"1","2","3","4","5","6","7","8"};
        //创建图
        Graph graph = new Graph(n);
        //循环的添加顶点
        for (String vertex : Vertexs) {
            graph.insertVertex(vertex);
        }
        /*//添加边 A-B A-C B-C B-D B-E
        graph.insertEdge(0,1,1);
        graph.insertEdge(0,2,1);
        graph.insertEdge(1,2,1);
        graph.insertEdge(1,3,1);
        graph.insertEdge(1,4,1);*/
        graph.insertEdge(0,1,1);
        graph.insertEdge(0,2,1);
        graph.insertEdge(1,3,1);
        graph.insertEdge(1,4,1);
        graph.insertEdge(3,7,1);
        graph.insertEdge(4,7,1);
        graph.insertEdge(2,5,1);
        graph.insertEdge(2,6,1);
        graph.insertEdge(5,6,1);


        //打印图矩阵
        graph.showGraph();

        //深度优先遍历
        System.out.println("深度优先遍历结果:");
        graph.dfs();//A->B->C->D->E->
        System.out.println();

        //广度优先遍历
        System.out.println("广度优先遍历结果:");
        graph.bfs();//A->B->C->D->E->


    }

    private ArrayList<String> vertexList;//存储顶点集合
    private int[][] edges;//存储图对应的邻接矩阵
    private int numOfEdges;//表示边的数目
    //定义数组,记录某个顶点是否被访问过
    private boolean isVisited[];

    //构造器
    public Graph(int n) {//n顶点的个数
        //初始化矩阵和vertexList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);

    }

    //插入顶点
    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    //添加边
    /**
     *
     * @param v1 第一个顶点的下标,即是第几个顶点 "A"-"B" 0-1
     * @param v2 第二个顶点的下标
     * @param weight 值
     */
    public void insertEdge(int v1,int v2,int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }

    //返回节点的个数
    public int getNumOfVertex() {
        return vertexList.size();
    }

    //得到边的数目
    public int getNumOfEdges() {
        return numOfEdges;
    }

    //返回节点 i(下标)对应的数据 0->"A"
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    //返回v1和v2的权值
    public int getWeight(int v1,int v2) {
        return edges[v1][v2];
    }

    //显示图对应的矩阵
    public void showGraph() {
        for (int[] link : edges) {
            System.out.println(Arrays.toString(link));
        }
    }

    //得到第一个邻接节点的下标w

    /**
     *
     * @param index
     * @return 如果存在就返回对应的下标,否则返回-1
     */
    public int getFirstNeighbor(int index) {
        for (int j = 0; j < vertexList.size(); j++) {
            if (edges[index][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    //根据前一个邻接节点的下标来获取下一个邻接节点
    public int getNextNeighbor(int v1,int v2) {
        for (int j = v2 + 1; j < vertexList.size(); j++) {
            if (edges[v1][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    //深度优先遍历
    //对一个节点的深度优先遍历
    private void dfs(boolean[] isVisited,int i) {
        //首先我们访问该节点,输出
        System.out.print(getValueByIndex(i) + "->");
        //将该节点设置为已访问
        isVisited[i] = true;

        //查找节点i的第一个邻接节点w
        int w = getFirstNeighbor(i);
        while (w != -1) { //w存在
            if (!isVisited[w]) {//如果w还未被访问
                dfs(isVisited,w);
            }
            //如果w已经被访问
            w = getNextNeighbor(i,w);
        }
    }

    //对dfs进行重载,遍历所有的节点,并进行dfs
    public void dfs() {
        isVisited = new boolean[8];
        //遍历所有的节点,进行dfs【回溯】
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                dfs(isVisited,i);
            }
        }
    }

    //广度优先遍历
    //对一个节点的广度优先遍历
    private void bfs(boolean[] isVisited,int i) {
        int u;//表示队列的头结点对应的下标
        int w;//邻接节点w
        //队列
        LinkedList queue = new LinkedList();//addLast removeFirst

        //访问节点
        //首先我们访问该节点,输出
        System.out.print(getValueByIndex(i) + "->");
        //将该节点设置为已访问
        isVisited[i] = true;

        //将这个节点加入队列
        queue.addFirst(i);

        while (!queue.isEmpty()) {
            //取出队列的头结点下标
            u = (Integer)queue.removeFirst();//自动拆箱
            //得到第一个邻接节点的下标w
            w = getFirstNeighbor(u);
            while (w != -1) {
                //是否访问过
                if (!isVisited[w]) {
                    System.out.print(getValueByIndex(w) + "->");
                    isVisited[w] = true;
                    //入队
                    queue.addLast(w);
                }
                //以u为前驱,找w后面的下一个邻接点
                w = getNextNeighbor(u,w);//体现出我们的广度优先
            }
        }
    }

    //重载广度优先,遍历所有的节点都进行广度优先搜索
    public void bfs() {
        isVisited = new boolean[8];
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                bfs(isVisited,i);
            }
        }
    }




}

程序员常用10种算法

二分查找算法(非递归)

二分查找算法(非递归)介绍

前面我们讲过了二分查找算法,是使用递归的方式,下面我们讲解二分查找算法的非递归方式
二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
二分查找法的运行时间为对数时间O(㏒₂n) ,即查找到需要的目标位置最多只需要㏒₂n步,假设从[0,99]的队列(100个数,即n=100)中寻到目标数30,则需要查找步数为㏒₂100 , 即最多需要查找7次( 2^6 < 100 < 2^7)

二分查找算法(非递归)代码实现

数组 {1,3, 8, 10, 11, 67, 100}, 编程实现二分查找, 要求使用非递归的方式完成.

思路分析:
代码实现:

package com.zcr.Algorithm;

/**
 * @author zcr
 * @date 2019/7/10-10:41
 */
public class BinarySearchNoRecur {

    public static void main(String[] args) {
        int arr[] = {1,3,8,10,11,67,100};
        int index = binarySearch(arr,2);
        System.out.println(index);

    }

    //二分查找的非递归实现

    /**
     *
     * @param arr 带查找的数组,arr是升序排列
     * @param target 需要查找的数
     * @return 返回对应下标,-1表示没有找到
     */
    public static int binarySearch(int[] arr,int target){
        int left = 0;
        int right = arr.length - 1;
        while (left <= right) {//说明可以继续查找
            int mid = (left + right) / 2;
            if (arr[mid] == target) {
                return mid;
            } else if (arr[mid] > target) {
                right = mid - 1;//需要向左边查找
            } else {
                left = mid + 1;//需要向右边查找
            }
        }
        return -1;
    }
}

分治算法

分治算法介绍
分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
分治算法可以求解的一些经典问题
二分搜索
大整数乘法
棋盘覆盖
合并排序
快速排序
线性时间选择
最接近点对问题
循环赛日程表
汉诺塔

分治算法的基本步骤
分治法在每一层递归上都有三个步骤:
分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
合并:将各个子问题的解合并为原问题的解。

分治(Divide-and-Conquer§)算法设计模式如下:
在这里插入图片描述
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。
ADHOC§是该分治法中的基本子算法,用于直接解小规模的问题P。
因此,当P的规模不超过n0时直接用算法ADHOC§求解。
算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。

分治算法最佳实践-汉诺塔
汉诺塔的传说
汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
假如每秒钟一次,共需多长时间呢?移完这些金片需要5845.54亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了5845.54亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。

分治算法最佳实践-汉诺塔
汉诺塔游戏的演示和思路分析:
(1)如果是有一个盘, A->C
如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的盘 2. 上面的盘
先把 最上面的盘 A->B
把最下边的盘 A->C
把B塔的所有盘 从 B->C
在这里插入图片描述
1个盘
在这里插入图片描述
2个盘
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
不管有多少盘,都可以看成是只有两个盘
3个盘
在这里插入图片描述
先把上面两个盘移动到B塔
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
4个盘
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
第一步完成
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.zcr.Algorithm;

/**
 * @author zcr
 * @date 2019/7/10-11:03
 */
public class DivideAndConquer {

    public static void main(String[] args) {
        Hanoitower.hanoiTower(5,'A','B','C');


    }
}

class Hanoitower {

    //汉诺塔的移动的方法
    //使用分治算法

    /**
     *
     * @param num 有多少个盘
     * @param a 柱子1
     * @param b 柱子2
     * @param c 柱子3
     */
    public static void hanoiTower(int num,char a,char b,char c) {
        //如果只有一个盘
        if (num == 1) {
            System.out.println("第1个盘从" + a + "->" + c);
        } else {
            //如果我们有n>=2个盘,我们总是可以看做:最下边一个盘、上面的所有盘
            //1.先把最上面的所有盘A-B,移动过程会使用到C
            hanoiTower(num - 1,a,c,b);
            //2.把最下边的盘A-C
            System.out.println("第" + num + "个盘从" + a +"->" + c);
            //3.把B塔所有盘移动到C,B-C,移动过程中使用A
            hanoiTower(num - 1,b,a,c);
        }
    }
}

动态规划算法

应用场景-背包问题
背包问题:有一个背包,容量为4磅 , 现有如下物品
在这里插入图片描述

要求达到的目标为装入的背包的总价值最大,并且重量不超出要求装入的物品不能重复
0-1背包问题

动态规划算法介绍
1.动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
2.动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
3.与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
4.动态规划可以通过填表的方式来逐步推进,得到最优解.

动态规划算法最佳实践-背包问题
思路分析和图解
背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)
这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。

动态规划算法最佳实践-背包问题
思路分析和图解
算法的主要思想,利用动态规划来解决。

每次遍历到的第i个物品,根据w[i]和v[i]
来确定是否需要将该物品放入背包中。
w[i]:第i个物品的重量 v[i]“第i个物品的价值”

即对于给定的n个物品,设v[i]、w[i]
分别为第i个物品的价值和重量,C为背包的容量。

再令v[i][j]
表示在前i个物品中能够装入容量为j的背包中的最大价值

则我们有下面的结果:

(1)  v[i][0]=v[0][j]=0; 
//表示 填入表 第一行和第一列是0
(2) 当w[i]> j 时:v[i][j]=v[i-1][j]   
// 当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个单元格的装入策略
(3) 当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}  
// 当 准备加入的新增的商品的容量小于等于当前背包的容量,
// 装入的方式:
v[i-1][j]: 就是上一个单元格的装入的最大值
v[i] : 表示当前商品的价值 
v[i-1][j-w[i]] : 装入i-1商品,到剩余空间j-w[i]的最大值
当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} : 

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

// 当 准备加入的新增的商品的容量小于等于当前背包的容量,
    // 装入的方式:
    v[i-1][j]: 就是上一个单元格的装入的最大值
    v[i] : 表示当前商品的价值 
    v[i-1][j-w[i]] : 装入i-1商品,到剩余空间j-w[i]的最大值
    当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} : 

就比较一下:旧的商品的价值 当前商品的价值+旧的的商品撞到剩余空间

package com.zcr.Algorithm;

/**
 * @author zcr
 * @date 2019/7/10-11:48
 */
public class Dynamic {

    public static void main(String[] args) {
        int[] w = {1,4,3};//物品的重量
        int[] val = {1500,3000,2000};//物品的价值 //这里的val[i]就是前面讲的v[i]
        int m = 4;//背包的重量
        int n = val.length;//物品的个数



        //创建二维数组,v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值
        int[][] v= new int [n + 1][m + 1];//物品的个数加1,背包的重量加1

        //为了记录放入商品的情况,我们定义一个二维数组
        int[][] path = new int[n + 1][m + 1];

        //初始化第一行和第一列,在本程序中,可以不去处理,因为默认为0
        for (int i = 0; i < v.length; i++) {
            v[i][0] = 0;//将第一列设置为0
        }
        for (int i = 0; i < v[0].length; i++) {
            v[0][i] = 0;//将第一行置为0
        }

        //根据前面得到的公式来动态规划处理
        for (int i = 1; i < v.length; i++) {//不处理第一行
            for (int j = 1; j < v[0].length; j++) {//不处理第一列
                //公式
                if (w[i - 1] > j) {//因为程序的i是从1开始的,因此原来的公式中的w[i]要修修改成w[i-1]
                    v[i][j] = v[i - 1][j];
                } else {
                    //因为我们的i是从1开始的,因此原来被公式中的val[i]改为val[i-1]
                    //v[i][j] = Math.max(v[i - 1][j],val[i - 1] + v[i - 1][j - w[i - 1]]);
                    //为了记录商品存放到背包的情况,我们不能直接的使用上面的公式,需要使用if-else来体现公式
                    if (v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
                        v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
                        //把当前的情况记录到path
                        path[i][j] = 1;
                    } else {
                        v[i][j] = v[i - 1][j];
                    }
                }

            }

        }

        //输出一下v 看看目前的情况
        for (int i = 0; i < v.length; i++) {
            for (int j = 0; j < v[i].length; j++) {
                System.out.print(v[i][j] + " ");
            }
            System.out.println();
        }

        //动态规划的结果
        //记录存放的路径
        //输出最后我们是放入的哪些商品
        /*for (int i = 0; i < path.length; i++) {
            for (int j = 0; j < path[i].length; j++) {
                if (path[i][j] == 1) {
                    System.out.printf("第%d个商品存到背包\n",i);

                }
            }
        }*/
        //遍历path,这样输出就会把所有的放入情况都得到,其实我们只需要最后的放入
        int i = path.length - 1;//行的最大下标
        int j = path[0].length - 1;//列的最大下标
        while (i > 0 && j > 0) {//从path的最后开始找
            if (path[i][j] == 1) {
                System.out.printf("第%d个商品存到背包\n",i);
                j -= w[i - 1];
            }
            i--;

        }




    }
}

class KnapsackProblem {

}

“C:\Program Files\Java\jdk1.8.0_181\bin\java.exe” “-javaagent:D:\IntelliJ IDEA 2018.2.5\lib\idea_rt.jar=53963:D:\IntelliJ IDEA 2018.2.5\bin” -Dfile.encoding=UTF-8 -classpath “C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;E:\Architect\1\DataStructure\out\production\DataStructure” com.zcr.Algorithm.Dynamic
0 0 0 0 0
0 1500 1500 1500 1500
0 1500 1500 1500 3000
0 1500 1500 2000 3500
第3个商品存到背包
第1个商品存到背包

Process finished with exit code 0

KMP算法

应用场景-字符串匹配问题

字符串匹配问题::
有一个字符串 str1= ““硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好””,和一个子串 str2=“尚硅谷你尚硅你”
现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1

暴力匹配算法
如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:
如果当前字符匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个字符
如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。(不可行!)
暴力匹配算法实现.

package com.zcr.Algorithm;

/**
 * @author zcr
 * @date 2019/7/10-16:34
 */
public class ViolenceMatch {

    public static void main(String[] args) {
        //测试暴力匹配算法
        String str1 = "我是地球人 你好 你好 你好";
        String str2 = "你好 你好";
        int index = violenceMatch(str1,str2);
        System.out.println(index);

    }

    //暴力匹配算法的实现
    public static int violenceMatch(String str1,String str2) {
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();

        int s1Len = s1.length;
        int s2Len = s2.length;

        int i = 0;//i索引指向s1
        int j = 0;//j索引指向s2
        while (i < s1Len && j < s2Len) {//保证匹配时不越界
            if (s1[i] == s2[j]) {//匹配成功
                i++;
                j++;
            } else {//没有匹配成功
                i = i - (j - 1);
                j = 0;
            }
        }

        //判断是否匹配成功
        if (j == s2Len) {
            return i - j;
        } else {
            return -1;
        }
    }
}

KMP算法介绍
KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法.
KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间
参考资料:https://www.cnblogs.com/ZuoAndFutureGirl/p/9028287.html

KMP算法最佳应用-字符串匹配问题
字符串匹配问题::
有一个字符串 str1= “BBC ABCDAB ABCDABCDABDE”,和一个子串 str2=“ABCDABD”
现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
要求:使用KMP算法完成判断,不能使用简单的暴力匹配算法.
思路分析图解
看老师代码实现
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

package com.zcr.Algorithm;

import java.util.Arrays;

/**
 * @author zcr
 * @date 2019/7/10-17:10
 */
public class KMPMatch {

    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";

        int[] next = kmpnext(str2);
        System.out.println(Arrays.toString(next));//[0,1,2]

        int index = kmpSearch(str1,str2,next);
        System.out.println(index);
    }

    //获取到一个字符串(子串)的部分匹配值表
    public static int[] kmpnext(String dest) {

        //创建一个next数组,保存部分匹配值
        int[] next = new int[dest.length()];
        //如果dest的长度为1,那么它的部分匹配值就是0
        next[0] = 0;
        for (int i = 1,j = 0; i < dest.length(); i++) {
            //当dest.charAt(i) != dest.charAt(j)
            //我们需要从next[j - 1]获取新的j
            //直到我们发现有dest.charAt(i) == dest.charAt(j)成立才退出
            while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
                j = next[j - 1];//这是KMP算法的核心点
            }
            //部分匹配值就需要加1
            if (dest.charAt(i) == dest.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }

    //KMP搜索算法

    /**
     *
     * @param str1 源字符串
     * @param str2 子字符串
     * @param next 部分匹配表,是子串对应的部分匹配表
     * @return 如果是-1就是没有匹配到,否则返回第一个匹配的位置
     */
    public static int kmpSearch(String str1,String str2,int[] next) {
        //遍历
        for (int i = 0,j = 0; i < str1.length(); i++) {

            //需要处理当这两个不相等的时候该怎么走,去调整j的大小
            //KMP算法的核心点
            while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
                j = next[j - 1];
            }


            if (str1.charAt(i) == str2.charAt(j)) {
                j++;
            }

            if (j == str2.length()) {//找到了
                return i - j + 1;
            }
        }
        return -1;

    }


}

https://www.cnblogs.com/ZuoAndFutureGirl/p/9028287.html

贪心算法

应用场景-集合覆盖问题

假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号
在这里插入图片描述
贪心算法介绍
贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法
贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果

贪心算法最佳应用-集合覆盖
思路分析:
如何找出覆盖所有地区的广播台的集合呢,使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集
假设总的有n个广播台,则广播台的组合总共有2ⁿ -1 个,假设每秒可以计算10个子集, 如图:
在这里插入图片描述
贪心算法最佳应用-集合覆盖
思路分析:
使用贪婪算法,效率高:
目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:
1.遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系)
2.将这个电台加入到一个集合中(比如ArrayList), 想办法把该电台覆盖的地区在下次比较时去掉。
3.重复第1步直到覆盖了全部的地区
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 代码实现, 看老师演示

    package com.zcr.Algorithm;

    import java.util.*;

    /**

    • @author zcr

    • @date 2019/7/10-19:35
      */
      public class Greedy {

      public static void main(String[] args) {

       //创建广播电台,放入到一个Map中
       Map<String, HashSet<String>> broadcasts = new HashMap<String, HashSet<String>>();
       //将各个电台放入到broadcasts
       HashSet<String> hashSet1 = new HashSet<String>();
       hashSet1.add("北京");
       hashSet1.add("上海");
       hashSet1.add("天津");
       HashSet<String> hashSet2 = new HashSet<String>();
       hashSet2.add("广州");
       hashSet2.add("北京");
       hashSet2.add("深圳");
       HashSet<String> hashSet3 = new HashSet<String>();
       hashSet3.add("成都");
       hashSet3.add("上海");
       hashSet3.add("深圳");
       HashSet<String> hashSet4 = new HashSet<String>();
       hashSet4.add("上海");
       hashSet4.add("天津");
       HashSet<String> hashSet5 = new HashSet<String>();
       hashSet5.add("杭州");
       hashSet5.add("大连");
       //加入到map
       broadcasts.put("K1",hashSet1);
       broadcasts.put("K2",hashSet2);
       broadcasts.put("K3",hashSet3);
       broadcasts.put("K4",hashSet4);
       broadcasts.put("K5",hashSet5);
      
       //存放所有的地区
       Set<String> allAreas = new HashSet<String>();
       /*allAreas.add("北京");
       allAreas.add("上海");
       allAreas.add("天津");
       allAreas.add("广州");
       allAreas.add("成都");
       allAreas.add("杭州");
       allAreas.add("深圳");
       allAreas.add("大连");*/
      /* for (Map.Entry<String, HashSet<String>> entry : broadcasts.entrySet()) {
           for (String city : entry.getValue()) {
               if (!allAreas.contains(city)) {
                   allAreas.add(city);
               }
           }
       }
       System.out.println(allAreas);*/
       for (String key : broadcasts.keySet()) {
           for (String city : broadcasts.get(key)) {
               //if (!allAreas.contains(city)) {
                   allAreas.add(city);
               //}
           }
       }
       System.out.println(allAreas);
      
      
      
       //创建列表,存放选择的电台集合
       List<String> selects = new ArrayList<String>();
      
       //定义一个临时的集合,在遍历的过程中存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的交集
       Set<String> tempSet = null;
      
       //保存在一次遍历过程中,能够覆盖最多未覆盖的地区,对应的电台的k
       //如果maxKey不为空,则会加入到selects
       String maxKey;
      
       while (allAreas.size() != 0) {//allAreas是不断减少的,不为0则表示还没有覆盖到所有地区
           //每进行一次while,就需要
           maxKey = null;
      
           //遍历broadcasts,取出对应的key
           for (String key : broadcasts.keySet()) {
               //每进行一次for,就需要
               tempSet = new HashSet<String>();
      
               //当前这个key能够覆盖的地区
               Set<String> areas = broadcasts.get(key);
               tempSet.addAll(areas);
               //求出tempSet和allAreas两个集合的交集,交集会赋值给tempSet
               tempSet.retainAll(allAreas);
               //如果当前集合包含的未覆盖地区的数量,比maxKey指向的集合未覆盖的地区还多,就需要重置maxKey
               if (tempSet.size() > 0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())) {//每次都选择最优的
                   maxKey = key;
               }
           }
           //maxKey != null,就应该将maxKey加入selects
           if (maxKey != null) {
               selects.add(maxKey);
               //将maxKey指向的广播电台覆盖的地区从allAreas中去掉
               allAreas.removeAll(broadcasts.get(maxKey));
           }
       }
      
       System.out.println("得到的选择结果是:"+selects);
      

      }

    }

贪心算法注意事项和细节

贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
比如上题的算法选出的是K1, K2, K3, K5,符合覆盖了全部的地区
但是我们发现 K2, K3,K4,K5 也可以覆盖全部地区,如果K2 的使用成本低于K1,那么我们上题的 K1, K2, K3, K5 虽然是满足条件,但是并不是最优的.

普里姆算法

在这里插入图片描述
应用场景-修路问题

看一个应用场景和问题:
有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通
各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
思路: 将10条边,连接即可,但是总的里程数不是最小.
正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少.

最小生成树
修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称MST。
1.给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
2.N个顶点,一定有N-1条边
3.包含全部顶点
4.N-1条边都在图中
5.举例说明(如图:)
6.求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法
在这里插入图片描述
普里姆算法介绍
普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图

普利姆的算法如下:
1.设G=(V,E)是连通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
2.若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1
3.若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中,将边(ui,vj)加入集合D中,标记visited[vj]=1
4.重复步骤②,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边
提示: 单独看步骤很难理解,我们通过代码来讲解,比较好理解.
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
普里姆算法最佳实践(修路问题)
在这里插入图片描述
有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通
各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
看老师思路分析+代码演示:

package com.zcr.graph;

import java.util.Arrays;

/**
 * @author zcr
 * @date 2019/7/10-21:53
 */
public class Prim {

    public static void main(String[] args) {

        //创建图
        //顶点的值
        char[] data = new char[]{'A','B','C','D','E','F','G'};
        int verxs = data.length;
        //邻接矩阵的关系使用二维数组表示
        //10000这个大数字用来表示两个点之间不连通
        int[][] weight = new int[][]{
                {10000,5,7,10000,10000,10000,2},
                {5,10000,10000,9,10000,10000,3},
                {7,10000,10000,10000,8,10000,10000},
                {10000,9,10000,10000,10000,4,10000},
                {10000,10000,8,10000,10000,5,4},
                {10000,10000,10000,4,5,10000,6},
                {2,3,10000,10000,4,6,10000},
        };

        //创建MGraph对象
        MGraph graph = new MGraph(verxs);
        //创建一个MinTree对象
        MinTree minTree = new MinTree();
        minTree.createGraph(graph,verxs,data,weight);

        //输出
        minTree.showGraph(graph);

        //prim算法
        minTree.prim(graph,0);


    }
}

//创建最小生成树
class MinTree {
    //创建图的邻接矩阵
    /**
     *
     * @param graph 图对象
     * @param verxs 图的顶点个数
     * @param data 图的顶点的值
     * @param weight 图的邻接矩阵
     */
    public void createGraph(MGraph graph,int verxs,char data[],int[][] weight) {
        int i, j;
        for (i = 0; i < verxs; i++) {//遍历顶点
            graph.data[i] = data[i];
            for (j = 0; j < verxs; j++) {
                graph.weigth[i][j] = weight[i][j];
            }
        }
    }

    //显示图的方法,显示图的邻接矩阵
    public void showGraph(MGraph graph) {
        for (int[] link : graph.weigth) {
            System.out.println(Arrays.toString(link));
        }
    }

    //编写Prim算法,得到最小生成树
    /**
     *
     * @param graph 图
     * @param v 表示从图的第几个顶点开始生成
     */
    public void prim(MGraph graph,int v) {

        //标记节点是否被访问过
        int visited[] = new int[graph.verx];
        //visited[]默认元素的值都是0,表示都没有访问过,所以下面这个循环赋初值可以不写
        for (int i = 0; i < graph.verx; i++) {
            visited[i] = 0;
        }

        //把当前这个节点标记为已访问
        visited[v] = 1;
        //h1、h2记录两个顶点的下标
        int h1 = -1;
        int h2 = -1;
        int minWeight = 10000;//将这个初始化为最大值,后面在遍历过程中会被替换
        for (int k = 1; k < graph.verx; k++) {//生成 (顶点个数-1) 条边

            //确定每一次生成的子图和哪个节点的距离最近
            for (int i = 0; i < graph.verx; i++) {//i表示被访问过的节点
                for (int j = 0; j < graph.verx; j++) {//j表示还没有访问过的节点
                    if (visited[i] == 1 && visited[j] == 0 && graph.weigth[i][j] < minWeight) {
                        //替换minWeight,寻找已经访问过的节点和未访问过的节点间的权值最小的边
                        minWeight = graph.weigth[i][j];
                        h1 = i;
                        h2 = j;
                    }
                }
            }
            //找到一条边是最小
            System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + "权值:" + minWeight);
            //将当前这个找到的节点标记为已经访问
            visited[h2] = 1;
            //重置minWeight为一个最大值
            minWeight = 10000;
        }

    }




}

class MGraph {
    int verx;//表示图中节点个数
    char[] data;//存放节点数据
    int[][] weigth;//存放边,就是我们的邻接矩阵

    public MGraph(int verx) {
        this.verx = verx;
        data = new char[verx];
        weigth = new int[verx][verx];
    }
}

克鲁斯卡尔算法

应用场景-公交站问题

看一个应用场景和问题:
在这里插入图片描述

某城市新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通
各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里
问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

克鲁斯卡尔算法介绍
克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止

克鲁斯卡尔算法图解说明
以城市公交站问题来图解说明 克鲁斯卡尔算法的原理和步骤:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

克鲁斯卡尔最佳实践-公交站问题
看一个公交站问题:
在这里插入图片描述
有北京有新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通
各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里
问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

package com.zcr.graph;

import java.util.Arrays;

/**
 * @author zcr
 * @date 2019/7/10-22:46
 */
public class Kruskal {

    private int edgeNum;//边的个数
    private char[] vertexs;//顶点数组
    private int[][] matrix;//邻接矩阵
    private static final int INF = Integer.MAX_VALUE;//表示两个顶点不能联通


    public static void main(String[] args) {

        char[] vertexs = {'A','B','C','D','E','F','G'};
        int matrix[][] = {
                {0,12,INF,INF,INF,16,14},
                {12,0,10,INF,INF,7,INF},
                {INF,10,0,3,5,6,INF},
                {INF,INF,3,0,4,INF,INF},
                {INF,INF,5,4,0,2,8},
                {16,7,6,INF,2,0,9},
                {14,INF,INF,INF,8,9,0},
        };

        //创建克鲁斯卡尔
        Kruskal kruskal = new Kruskal(vertexs,matrix);
        //输出构建的图的邻接矩阵
        kruskal.print();

        //返回所有边的数组
        System.out.println("未排序的边的数组" + Arrays.toString(kruskal.getEdges()));
        System.out.println("所有边的个数" + kruskal.edgeNum);

        //将边排序
        EData[] edges = kruskal.getEdges();
        kruskal.sortEdges(edges);
        System.out.println("排序后的边的数组" + Arrays.toString(edges));

        kruskal.kruskal();

    }

    //构造器
    public Kruskal(char[] vertexs,int[][] matrix) {
        //初始化顶点个数和边的个数
        int vlen = vertexs.length;

        //初始化顶点,用的是复制拷贝的方式
        //this.vertex = vertexs;也可以这样写
        this.vertexs = new char[vlen];
        for (int i = 0; i < vlen; i++) {
            this.vertexs[i] = vertexs[i];
        }

        //初始化边,用的是复制拷贝的方式
        this.matrix = new int[vlen][vlen];
        for (int i = 0; i < vlen; i++) {
            for (int j = 0; j < vlen; j++) {
                this.matrix[i][j] = matrix[i][j];
            }
        }

        //统计边
        for (int i = 0; i < vlen; i++) {
            for (int j = i + 1; j < vlen; j++) {
                if (this.matrix[i][j] != INF) {
                    edgeNum++;
                }
            }
        }
    }


    //打印邻接矩阵
    public void print() {
        System.out.println("邻接矩阵为:\n");
        for (int i = 0; i < vertexs.length; i++) {
            for (int j = 0; j < vertexs.length; j++) {
                System.out.printf("%12d \t",matrix[i][j]);
            }
            System.out.println();
        }
    }

    //对边进行排序,冒泡排序
    /**
     *
     * @param edges 边的集合
     */
    private void sortEdges(EData[] edges) {
        for (int i = 0; i < edges.length - 1; i++) {
            for (int j = 0; j < edges.length - 1 - i; j++) {
                if (edges[j].weight > edges[j + 1].weight) {//交换
                    EData tmp = edges[j];
                    edges[j] = edges[j + 1];
                    edges[j + 1] = tmp;
                }
            }
        }
    }

    /**
     *
     * @param ch 顶点的值,比如'A'
     * @return 返回顶点对应的下标,如果找不到返回-1
     */
    private int getPosition(char ch) {
        for (int i = 0; i < vertexs.length; i++) {
            if (vertexs[i] == ch) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 获取图中的边,放到EData[]数组中,后面我们需要遍历该数组
     * 是通过matrix邻接矩阵来获取
     * EData[] 形式[['A','B',12],['B','F',3],...]
     * @return
     */
    private EData[] getEdges() {
        int index = 0;
        EData[] edges = new EData[edgeNum];
        for (int i = 0; i < vertexs.length; i++) {
            for (int j = i + 1; j < vertexs.length; j++) {
                if (matrix[i][j] != INF) {
                    edges[index++] = new EData(vertexs[i],vertexs[j],matrix[i][j]);
                }
            }
        }
        return edges;
    }

    /**
     * 功能:获取下标为i的顶点的终点(判断是否有回路,就是判断两个顶点的终点是否是同一个)
     *
     * @param ends 这个数组记录了各个顶点对应的终点是哪个,是在我们遍历过程中逐步形成的
     * @param i 表示传入的顶点对应的下标
     * @return 返回的就是下标为i的这个顶点对应的终点的下标
     */
    private int getEnd(int[] ends,int i) {//5
        while (ends[i] != 0) {
            i = ends[i];
        }
        return i;

    }

    //克鲁斯卡尔算法生成最小生成树
    public void kruskal() {
        int index = 0;//表示最后结果数组的索引
        int[] ends = new int[edgeNum];//用于保存已有最小生成树中的每个顶点它在最小生成树中的终点
        //创建结果数组,保存最后的最小生成树
        EData[] rets = new EData[edgeNum];

        //获取图汇总所有边的集合,一共有12条边
        EData[] edges = getEdges();
        System.out.println("图的边的集合=" + Arrays.toString(edges) + "共" + edges.length +"条边");

        //按照边的权值从小到大金星排序
        sortEdges(edges);

        //遍历edges数组,将边添加到最小生成树中时,判断准备加入的边是否构成了回路,如果没有就加入到结果数组中去,否则不能加入
        for (int i = 0; i < edgeNum; i++) {
            //获取到第i条边的第一个顶点(起点)
            int p1 = getPosition(edges[i].start);//4
            //获取到第i条边的第二个顶点(终点)
            int p2 = getPosition(edges[i].end);//5

            //获取p1这个顶点它在已有最小生成树中的终点是哪一个
            int m = getEnd(ends,p1);//4
            //获取p2这个顶点它在已有最小生成树中的终点是哪一个
            int n = getEnd(ends,p2);//5

            //判断是否构成回路
            if (m != n) {//没有构成回路
                ends[m] = n;//设置m在已有最小生成树中的终点    [0,0,0,0,5,0,0,0,0,0,0,0,0,0]
                rets[index++] = edges[i];//有一条边加入到rets数组中了
            }
        }

        //统计并打印最小生成树,输出rets数组
        System.out.println("最小生成树为=");
        for (int i = 0; i < index; i++) {
            System.out.println(rets[i]);
        }

    }


}

//创建一个类EData,它的对象实例就表示一条边
class EData {
    char start;//边的起点
    char end;//边的终点
    int weight;//边的权值

    //构造器
    public EData(char start,char end,int weight) {
        this.start = start;
        this.end = end;
        this.weight = weight;
    }

    //重写toString方法,便于输出这条边
    @Override
    public String toString() {
        return "EData{" + "start=" + start + ", end=" + end + ", weight=" + weight + '}';
    }
}

迪杰斯特拉算法

应用场景-最短路径问题

看一个应用场景和问题:
在这里插入图片描述
战争时期,胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄
各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
问:如何计算出G村庄到 其它各个村庄的最短距离?
如果从其它点出发到各个点的最短距离又是多少?

迪杰斯特拉(Dijkstra)算法介绍
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。

迪杰斯特拉(Dijkstra)算法过程
设置出发顶点为v,顶点集合V{v1,v2,vi…},v到V中各顶点的距离构成距离集合Dis,Dis{d1,d2,di…},Dis集合记录着v到图中各顶点的距离(到自身可以看作0,v到vi距离对应为di)
1.从Dis中选择值最小的di并移出Dis集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径
2.更新Dis集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为vi,表明是通过vi到达的)
3.重复执行两步骤,直到最短路径顶点为目标顶点即可结束
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

迪杰斯特拉(Dijkstra)算法最佳应用-最短路径
在这里插入图片描述

战争时期,胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄
各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
问:如何计算出G村庄到 其它各个村庄的最短距离?
如果从其它点出发到各个点的最短距离又是多少?

A(2)B(3)C(9)D(10)E(4)F(6)G(0)
A(7) B(12) C(0) D(17) E(8) F(13) G(9)

package com.zcr.graph;

import java.util.Arrays;

/**
 * @author zcr
 * @date 2019/7/11-12:59
 */
public class Dijkstra {

    public static void main(String[] args) {
        //顶点数组
        char[] vertex = {'A','B','C','D','E','F','G'};
        //邻接矩阵
        int[][] matrix = new int[vertex.length][vertex.length];
        final int N = 65535;//表示两个顶点之间不可连接
        matrix[0] = new int[]{N,5,7,N,N,N,2};
        matrix[1] = new int[]{5,N,N,9,N,N,3};
        matrix[2] = new int[]{7,N,N,N,8,N,N};
        matrix[3] = new int[]{N,9,N,N,N,4,N};
        matrix[4] = new int[]{N,N,8,N,N,5,4};
        matrix[5] = new int[]{N,N,N,4,5,N,6};
        matrix[6] = new int[]{2,3,N,N,4,6,N};

        //创建Graph
        Graph1 graph = new Graph1(vertex,matrix);
        //测试,看看图的邻接矩阵是否ok
        graph.showGraph();

        //测试
        graph.dijkstra(0);
        graph.showDijkstra();

        graph.dijkstra(1);
        graph.showDijkstra();

        graph.dijkstra(2);
        graph.showDijkstra();

        graph.dijkstra(3);
        graph.showDijkstra();

        graph.dijkstra(4);
        graph.showDijkstra();

        graph.dijkstra(5);
        graph.showDijkstra();

        graph.dijkstra(6);
        graph.showDijkstra();
        


    }
}

class Graph1 {
    private char[] vertex;//顶点数组
    private int[][] matrix;//邻接矩阵
    private VisitedVertex vv;//已经访问的节点的集合

    //构造器
    public Graph1(char[] vertex,int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }

    //显示图
    public void showGraph() {
        for (int[] link : matrix) {
            System.out.println(Arrays.toString(link));
        }
    }

    //Dijkstra算法

    /**
     *
     * @param index 表示出发顶点对应的下标
     */
    public void dijkstra(int index) {
        vv = new VisitedVertex(vertex.length,index);
        update(index);//更新index下标顶点到周围顶点的距离和前驱顶点

        for (int j = 1; j < vertex.length; j++) {
            index = vv.updateArr();//选择并返回新的访问顶点
            update(index);//更新index下标顶点到周围顶点的距离和前驱顶点
        }
    }

    //更新index下标顶点到周围顶点的距离和周围顶点的前驱节点
    private void update(int index) {
        int len = 0;
        //根据遍历我们的邻接矩阵的matrix[index]这一行
        for (int j = 0; j < matrix[index].length; j++) {
            //len含义是:出发顶点到index顶点的距离加上 从index顶点到j经典的距离的和
            len = vv.getDis(index) + matrix[index][j];
            //如果j这个顶点没有被访问过,并且len还小于出发顶点到j顶点的距离,就需要更新
            if (!vv.in(j) && len < vv.getDis(j)) {
                vv.updatePre(j,index);//更新j顶点的前驱为index
                vv.updateDis(j,len);//更新出发顶点到j顶点的距离
            }
        }
    }

    //显示结果
    public void showDijkstra() {
        vv.show();
    }


}

//已访问顶点集合
class VisitedVertex {
    //记录各个顶点是否访问过,1表示访问过,0表示未访问,会动态更新
    public int[] already_arr;
    //每个下标对应的值为前一个顶点下标,会动态更新
    public int[] pre_visited;
    //记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其他顶点的距离,会动态更新
    public int[] dis;

    //构造器
    /**
     *
     * @param length 顶点的个数
     * @param index 出发顶点对应的下标
     */
    public VisitedVertex(int length,int index) {
        this.already_arr = new int[length];
        this.pre_visited = new int[length];
        this.dis = new int[length];

        //初始化dis数组
        Arrays.fill(dis,65535);

        this.already_arr[index] = 1;//设置出发顶点被访问过
        this.dis[index] = 0;//设置出发顶点的访问距离为0
    }

    /**
     * 功能:判断某个顶点是否被访问过
     * @param index
     * @return 如果访问过返回true,否则返回false
     */
    public boolean in(int index) {
        return already_arr[index] == 1;
    }

    /**
     * 功能:更新出发顶点到index顶点的距离
     * @param index
     * @param len
     */
    public void updateDis(int index,int len) {
        dis[index] = len;
    }

    /**
     * 功能:更新pre这个顶点的前驱节点为index的节点
     * @param pre
     * @param index
     */
    public void updatePre(int pre,int index) {
        pre_visited[pre] = index;
    }

    /**
     * 功能:返回出发顶点到index顶点的距离
     * @param index
     */
    public int getDis(int index) {
        return dis[index];
    }

    //继续选择并返回新的访问顶点,比如这里的G完成后,就是A点作为访问节点
    public int updateArr() {
        int min = 65535,index = 0;
        for (int i = 0; i < already_arr.length; i++) {
            if (already_arr[i] == 0 && dis[i] < min){//i还没有被访问过
                min = dis[i];
                index = i;
            }
        }
        //更新index顶点被访问过
        already_arr[index] = 1;
        return index;
    }

    //显示最后的结果
    //即将三个数组的情况输出
    public void show() {
        System.out.println("===================");
        for (int i : already_arr) {
            System.out.print(i + " ");
        }
        System.out.println();

        for (int i : pre_visited) {
            System.out.print(i + " ");
        }
        System.out.println();

        for (int i : dis) {
            System.out.print(i + " ");
        }
        System.out.println();

        char[] vertex = {'A','B','C','D','E','F','G'};
        int count = 0;
        for (int i : dis) {
            if (i != 65535) {
                System.out.print(vertex[count] + "(" + i + ")");
            } else {
                System.out.println("N");
            }
            count++;
        }
        System.out.println();

    }



}

弗洛伊德算法

弗洛伊德(Floyd)算法介绍
和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名

弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径

弗洛伊德算法 VS 迪杰斯特拉算法:
迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径
弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径
在这里插入图片描述
弗洛伊德(Floyd)算法图解分析
1.设置顶点vi到顶点vk的最短路径已知为Lik,顶点vk到vj的最短路径已知为Lkj,顶点vi到vj的路径为Lij,则vi到vj的最短路径为:min((Lik+Lkj),Lij)vk的取值为图中所有顶点,则可获得vi到vj的最短路径
2.至于vi到vk的最短路径Lik或者vk到vj的最短路径Lkj,是以同样的方式获得
3.弗洛伊德(Floyd)算法图解分析-举例说明
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

弗洛伊德(Floyd)算法最佳应用-最短路径
在这里插入图片描述
胜利乡有7个村庄(A, B, C, D, E, F, G)
各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
问:如何计算出各村庄到 其它各村庄的最短距离?

package com.zcr.graph;

import java.util.Arrays;

/**
 * @author zcr
 * @date 2019/7/11-18:59
 */
public class Floyd {

    public static void main(String[] args) {

        //测试图是否创建成功
        char[] vertex = {'A','B','C','D','E','F','G'};
        //创建邻接矩阵
        int[][] matrix = new int[vertex.length][vertex.length];
        final int N = 65535;
        matrix[0] = new int[]{0,5,7,N,N,N,2};
        matrix[1] = new int[]{5,0,N,9,N,N,3};
        matrix[2] = new int[]{7,N,0,N,8,N,N};
        matrix[3] = new int[]{N,9,N,0,N,4,N};
        matrix[4] = new int[]{N,N,8,N,0,5,4};
        matrix[5] = new int[]{N,N,N,4,5,0,6};
        matrix[6] = new int[]{2,3,N,N,4,6,0};

        //创建图对象
        Graph2 graph2 = new Graph2(vertex.length,matrix,vertex);
        graph2.show();
        graph2.fliyed();
        graph2.show();

    }
}

//创建图
class Graph2 {
    private char[] vertex;//存放顶点的数组
    private int[][] dis;//保存从各个顶点出发到其他顶点的距离,最后的结果也是保留在此数组中的
    private int[][] pre;//保存到达目标顶点的前驱顶点

    //构造器
    /**
     *
     * @param length 顶点个数
     * @param martix 邻接矩阵
     * @param vertex 顶点数组
     */
    public Graph2(int length,int[][] martix,char[] vertex) {
        this.vertex = vertex;
        this.dis = martix;
        this.pre = new int[length][length];

        //对pre数组初始化,存放的是前驱顶点的下标'A'
        for (int i = 0; i < length; i++) {
            Arrays.fill(pre[i],i);
        }
    }

    //显示pre数组和dis数组
    public void show() {
        //为了显示便于阅读,优化输出
        char[] vertex = {'A','B','C','D','E','F','G'};
        for (int k = 0; k < dis.length; k++) {
            //先将pre数组的一行数组输出
            for (int i = 0; i < dis.length; i++) {
                System.out.print(vertex[pre[k][i]] + " ");
            }
            System.out.println();
            //输出dis数组的一行数据
            for (int i = 0; i < dis.length; i++) {
                System.out.print("("+vertex[k]+"到"+vertex[i]+"的最短路径是"+dis[k][i] + ")");
            }
            System.out.println();
        }
    }
    
    //弗洛伊德算法
    public void fliyed() {
        int len = 0;//变量保存距离
        
        //对中间顶点的遍历
        for (int k = 0; k < dis.length; k++) {
            //对出发节点的遍历
            for (int i = 0; i < dis.length; i++) {
                //对终点的遍历
                for (int j = 0; j < dis.length; j++) {
                    len = dis[i][k] + dis[k][j];//求出从i顶点出发,经过k中间顶点,到达顶点的距离
                    if (len < dis[i][j]) {
                        dis[i][j] = len;//更新距离
                        pre[i][j] = pre[k][j];//更新前驱顶点
                    }


                }
            }
        }
    }
}

马踏棋盘算法

马踏棋盘算法介绍和游戏演示

马踏棋盘算法也被称为骑士周游问题
将马随机放在国际象棋的8×8棋盘Board[0~7][0~7]的某个方格中,马按走棋规则**(马走日字**)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格
游戏演示: http://www.4399.com/flash/146267_2.htm
在这里插入图片描述
马踏棋盘游戏代码实现
马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。

如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了53个点,如图:走到了第53个,坐标(1,0),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯…… ,思路分析+代码实现

分析第一种方式的问题,并使用贪心算法(greedyalgorithm)进行优化。解决马踏棋盘问题.
使用前面的游戏来验证算法是否正确。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

package com.zcr.Algorithm;

import org.omg.PortableServer.POA;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Comparator;

/**
 * @author zcr
 * @date 2019/7/11-21:13
 */
public class HorseChessBoard {

    public static int X;//棋盘的列数
    public static int Y;//棋盘的行数

    //创建一个数组,标记棋盘的各个位置是否被访问过
    private static boolean visited[];
    //使用一个属性,标记是否棋盘的所有位置都被访问
    private static boolean finished;//true表示成功



    public static void main(String[] args) {
        //测试
        X = 8;
        Y = 8;
        int row = 1;//马儿初始位置的行,从1开始编号
        int column = 1;//
        //创建棋盘
        int[][] chessboard = new int[X][Y];
        visited =new boolean[X * Y];//初始值都是false
        //测试耗时
        long start = System.currentTimeMillis();
        traversalChessBoard(chessboard,row - 1,column - 1,1);
        long ends = System.currentTimeMillis();
        System.out.println("共耗时"+ (ends - start) + "ms");
        //输出棋盘的最后情况
        for (int[] rows : chessboard) {
            for (int step : rows) {
                System.out.print(step + "\t");
            }
            System.out.println();
        }

    }

    //马踏棋盘算法
    /**
     *
     * @param chessboard 棋盘
     * @param row 马儿当前的位置的行 从0开始
     * @param column 列
     * @param step 是第几步,初始位置就是第一步
     */
    public static void traversalChessBoard(int[][] chessboard,int row,int column,int step) {
        chessboard[row][column] = step;
        visited[row * X + column] = true;//row=4 X=8 column=4  4*8+4=36
        //获取当前位置可以走的下一个位置的集合
        ArrayList<Point> ps = next(new Point(column,row));
        //对ps进行排序,排序的规则就是对ps的所有的point对象的下一步的位置的数目,
        sort(ps);
        //遍历ps
        while (!ps.isEmpty()) {
            Point p = ps.remove(0);//取出一个可以走的位置
            //判断该店是否已经访问过
            if (!visited[p.y * X + p.x]) {//说明还没有访问过
                traversalChessBoard(chessboard,p.y,p.x,step + 1);
            }
        }
        //判断马儿是否完成了任务,使用atep和应该走的步数进行比较
        //如果没有达到数量,则表示没有完成任务,将整个棋盘置为0
        //说明:step < X * Y:棋盘到了目前为止仍然没有走完,或者棋盘处于一个回溯位置
        if (step < X * Y && !finished) {
            chessboard[row][column] = 0;
            visited[row * X + column] = false;
        } else {
            finished = true;
        }
    }





    //根据当前的位置,计算马儿还能走哪些位置,并放到一个集合中,最多有八个位置
    public static ArrayList<Point> next(Point curPoint) {
        //创建一个列表
        ArrayList<Point> ps = new ArrayList<Point>();
        //创建一个Point
        Point p1 = new Point();
        //表示马儿可以走5这个位置
        if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走6这个位置
        if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
            ps.add(new Point(p1));
        }
        // 判断马儿可以走7这个位置
        if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走0这个位置
        if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走1这个位置
        if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走2这个位置
        if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走3这个位置
        if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
            ps.add(new Point(p1));
        }
        //判断马儿可以走4这个位置
        if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
            ps.add(new Point(p1));
        }

        return ps;
    }

    //根据当前这一步的所有的下一步的选择的位置,进行非递减排序,减少回溯的次数
    public static void sort(ArrayList<Point> ps) {
        ps.sort(new Comparator<Point>() {
            @Override
            public int compare(Point o1, Point o2) {
                //先获取到o1这个点的下一步的所有位置的个数
                int count1 = next(o1).size();
                //先获取到o2这个点的下一步的所有位置的个数
                int count2 = next(o2).size();
                if (count1 < count2) {
                    return -1;
                } else if (count1 == count2) {
                    return 0;
                } else {
                    return 1;
                }

            }
        });
    }
}

7.1开场白 212

如果你不善于规划,很有可能就会出现如玩好新疆后到海南,然后再冲向黑龙江这样的荒唐决策。

7.2图的定义 213

现实中,人与人之间关系就非常复杂,比如我的认识的朋友,可能他们之间也互相认识,这就不是简单的一对一、一对多的关系了,那就是我们今天要研究的主题——图。

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通过表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合(有穷非空),E是图G中边的集合(可以为空)

• 线性表中我们把数据元素叫元素,树中将数据元素叫结点 ,在图中数据元 素,我们则称之为顶点 (Vertex)
• 线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。在图结构中,不允许没有顶点。在定义中,若 V 是顶 点的集合,则强调了顶点集合V有穷非空。
• 线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结 点具有层次关系,而图中,任意两个顶点之间都可能有关系,顶点之间的 逻辑关系用边来表示, 边集可以是空的。

图是一种较线性表和树更加复杂的数据结构,在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关

7.2.1各种图定义 214

无向边: 若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vii,vj)来表示,如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)

有向边: 若从顶点vi到vjj的边有方向,则称这条边为有向边,也称为弧(Arc),用有序偶<vi,vj>来表示,vi称为弧尾(Tail),vj称为弧头(Head),如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed graphs)

在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图

在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图,含有n个顶点的无向完全图有n×(n−1)/2条边

在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图,含有n个顶点的有向完全图有nx(n-1)条边

有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight),这种带权的图通常称为(Network)

假设有两个图G=(V,{E})和G’=(V’,{E’}),如果V’∈V且E’∈E,则称G’为G的子图(Subgraph)

7.2.2图的顶点与边间关系 217

对于无向图G=(V,{E}),如果边(v,v’)∈E,则称顶点v和v’互为邻接点(Adjacent),即v和v’相邻接,边(v,v’)依附(incident)于顶点v和v’,或者说(v,v’)与顶点v和v’相关联。顶点v的度(Degree)是和v相关联的边的数目,记为TD(v)
在这里插入图片描述
在这里插入图片描述

对于有向图G=(V,{E}),如果弧<v,v’>∈E,则称顶点v邻接到顶点v’,顶点v’邻接自顶点v。弧<v,v’>和顶点v,v’相关联,以顶点为头的弧的数目称为v的入度(InDegree),记为ID(v),以v为尾的弧的数目称为v的出度(OutDegree),记为OD(v);顶点v的度为TD(v)=ID(v)+OD(v)
在这里插入图片描述
在这里插入图片描述

树中根结点到任意结点的路径是唯一的, 但是图中顶点与顶点之间的路径却是不 唯一的。

路径的长度是路径上的边或弧的数目

第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle),序列中顶点不重复出现的路径称为简单路径,除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路,称为简单回路或简单环,如下左图为简单环,右图不是简单环
在这里插入图片描述

7.2.3连通图相关术语 219

无向图 G中,如果从顶点v到顶点v’有路径,则称v和v’是连通的,如果对于图中任意两个顶点vivi、vjvj∈E,vivi和vjvj都是连通的,则称G是 连通图(Connected Graph)

无向图中的极大连通子图称为连通分量,它强调:
要是子图
子图要是连通的
连通子图含有极大顶点数
具有极大顶点数的连通子图包含依附于这些顶点的所有边

有向图 G中,如果对于每一对vivi、vjvj∈V、vivi≠vjvj,从vivi到vjvj和从vjvj到vivi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量,如下图所示,左图并不是强连通图,右图是强连通图,且是左图的极大强连通子图,即是左图的强连通分量
在这里插入图片描述

一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边,如下图

在这里插入图片描述
的图 1 是一普通图,但显然不是生成树,当去掉两条构成环的边后,比如图 2 或图 3 ,就满足 n 个顶点 n-1 条边且连通的定义了。 它们都是一棵生成树。从这里也可知道,如果一个图有 n 个顶 点和小于n-1 条边,则是非连通图,如果官多于 n-1 边条,必定构成一个环, 因为 这条边使得它依附的那两个顶点之间有了第二条路径。比如图 2 和图 3 ,随便加哪两 顶点的边都将构成环。 不过有 n-1 条边并不一定是生成树,比如图 4。

如果一个有向图恰有一个顶点的入度为0(根结点),其余顶点的入度均为1,则是一棵有向树。对有向树的理解比较容易,所谓入度为 0 其实就相当于树中的根结点, 其余顶点入 度为 1 就是说树的非根结点的双亲只有一个。

一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧,如下图

如图 7-2-15 的 图 1 是一棵有向图。去掉一些弧后,它可以分解为两棵有向树,如图 2 和图 3 ,这两 棵就是图 1 有向图的生成森林。
在这里插入图片描述

7.2.4图的定义与术语总结 222

图按照有无方向分为无向图和有向图。无向图自顶点和边构成,有向图由顶点和 弧何成。弧有弧尾和弧头之分。

图按照边或弧的多少分稀疏图和稠密图。如果任意两个顶点之间都存在边叫完全 圈 , 有向的叫有向完全圈。若无重复的边或顶点到自身的边则叫简单图。

图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度,有向图顶点分 为入度和出度。

图上的边或弧上带权则称为网。

图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始 点则称为环, 当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图, 有向则称强连通图。 图中有子图 , 若子图极大连通则就是连通分量, 有向的则称强连通分量。

无向图中连通旦 n 个顶点 n-l 条边叫生成树。有向图中一顶点入度为0其余顶 点入度为 1 的叫有向树。 一个有向图由若干棵有向树构成生成森林。

7.3图的抽象数据类型 222

public interface IGraph<E> {
	 public int getNumOfVertex();//获取顶点的个数
	 boolean insertVex(E v);//插入顶点
	 boolean deleteVex(E v);//删除顶点
	 int indexOfVex(E v);//定位顶点的位置
	 E valueOfVex(int v);// 定位指定位置的顶点
	 boolean insertEdge(int v1, int v2,int weight);//插入边
	 boolean deleteEdge(int v1, int v2);//删除边
	 int getEdge(int v1,int v2);//查找边
	 String depthFirstSearch(int v );//深度优先搜索遍历
	 String breadFirstSearch(int v );//广度优先搜索遍历
	 public int[] dijkstra(int v);//查找源点到其它顶点的路径
}

数据元素:具有相同元素(结点)的数据集合;
数据结构:结点之间通过边或弧相互连接形成网络;
数据操作:对图的基本操作定义在IGraph中,代码如下:

7.4图的存储结构 223

因为美国的黑夜就是中国的白天,利用互联网,他的员工白天上班就可以监控到美国仓库夜间的实际情况,如果发生了像火灾、偷盗这样的突发事件,及时电话到美国当地相关人员处理

图的存储结构相较线性表与树来说就更加复杂了。 首先,我们口头上说的"顶点 的位置"或"~~接点的位置"只是一个相对的概念。 其实从图的逻辑结构定义来看, 图上任何一个顶点都可被看 '成是第一个顶点,任一顶点的专I~接点之间也不存在次序关 系。

也正由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数 据元素在内存中的物理位置来表示元素之问的关系,也就是说,图不可能用简单的顺 序存储结构来表示。

而多重链表的方式,即以一个数据域和多个指针域组成的结点表 示圈中的一个顶点,尽管可以实现图结构,但其实在树中,我们也已经讨论过,这是 有问题的。如果各个顶点的度数相差很大,按度数最大的顶点设计结点结构会造成很 多存储单元的浪费,而若按每个顶点自己的度数设计不同的顶点结构,又带来操作的 不便。

因此,对于图来说,如何对它实现物理存储是个难题,不过我们的前辈们已经 解决了,现在我们来看前辈们提供的五种不同的存储结构。

7.4.1邻接矩阵 224

将图分成顶点和边或弧两个结构来存储,顶点不分大小、主次,所以用一个一维数组来存储,而边或弧由于是顶点与顶点之间的关系,所以用二维数组(称为邻接矩阵)来存储。

图的邻接矩阵 (Adjacency Matrix) 存储方式是用两个数组来表示图。一个一维 数组存储圈中顶点信息,一个二维数组〈称为邻接矩阵)存储图中的边或弧的信息。
在这里插入图片描述
在这里插入图片描述

无向图的边数组是一个对称矩阵

有了这个矩阵,可以很容易地知道图中的信息:
很容易判断任意两顶点是否有边无边
某个顶点的度就是这个顶点vivi在邻接矩阵中第i行(或第i列)的元素之和,如v1v1的度就是1+0+1+0=2
求顶点vivi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点

有向图的边数组不是一个对称矩阵,有向图样例如下
在这里插入图片描述
在这里插入图片描述

网图是每条边上带有权的图,设图G是网图,有n个顶点,则邻接矩阵是一个nxn的方阵,定义为:
在这里插入图片描述

这里Wij表示(vi,vj)或<vi,vj>上的权值,∞表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值
在这里插入图片描述

缺点: 对于边数相对于顶点较少的图,这种结构是对存储空间的极大浪费

邻接矩阵是如何实现图的创建的呢?我们先来看看图的邻接矩阵存储的结 构,代码如下。

public class GraphAdjMatrix<E> implements IGraph<E> {
	private E[] vexs;// 存储图的顶点的一维数组
	private int[][] edges;// 存储图的边的二维数组
	private int numOfVexs;// 顶点的实际数量
	private int maxNumOfVexs;// 顶点的最大数量
	private boolean[] visited;// 判断顶点是否被访问过
 
	@SuppressWarnings("unchecked")
	public GraphAdjMatrix(int maxNumOfVexs, Class<E> type) {
		this.maxNumOfVexs = maxNumOfVexs;
		edges = new int[maxNumOfVexs][maxNumOfVexs];
		vexs
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值