数据结构和算法(Java),下

整个文章主要来源于尚硅谷韩顺平数据结构与算法以及加上我查阅资料后加上自己的理解编写而成,若发现有错误的地方,欢迎指正!

全文采用typora编辑而成,篇幅较大,此处为下部分,上部分

文章目录

第9章 树结构的基础部分

二叉树

为什么需要树这种数据结构

  1. 数组存储方式的分析

    1. 优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。

      缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低[示意图]

在这里插入图片描述

  1. 链式存储方式的分析

    优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。

    缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)

在这里插入图片描述

  1. 树存储方式分析

    能提高数据存储,读取的效率,比如利用二叉排序树(BinarySortTree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。【示意图,后面详讲】

​ 案例:[7,3,10,1,5,9,12]

在这里插入图片描述

树示意图

在这里插入图片描述

二叉树的概念

  1. 树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树。

  2. 二叉树的子节点分为左节点和右节点。

  3. 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。

    示意图:

在这里插入图片描述

  1. 一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树

    示意图:

在这里插入图片描述

从满二叉树和完全二叉树的定义可以看出, 满二叉树是完全二叉树的特殊形态, 即如果一棵二叉树是满二叉树, 则它必定是完全二叉树。

二叉树的遍历

二叉树遍历的简单介绍

使用前序,中序和后序对下面的二叉树进行遍历.

  1. 前序遍历:先输出父节点,再遍历左子树和右子树
  2. 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
  3. 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点

输出父节点的顺序,就确定是前序,中序还是后序

二叉树遍历实例

二叉树的前序,中序,后续的遍历步骤:

  1. 创建一颗二叉树
  2. 前序遍历
    • 先输出当前结点(初始的时候是root结点)
    • 如果左子结点不为空,则递归继续前序遍历
    • 如果右子结点不为空,则递归继续前序遍历
  3. 中序遍历
    • 如果当前结点的左子结点不为空,则递归中序遍历
    • 输出当前结点
    • 如果当前结点的右子结点不为空,则递归中序遍历
  4. 后续遍历
    • 如果当前结点的左子结点不为空,则递归后序遍历
    • 如果当前结点的右子结点不为空,则递归后续遍历
    • 输出当前结点

在这里插入图片描述

代码实现:

package cn.ysk.tree;

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

//        System.out.println("前序遍历:");
//        binaryTree.preOrder();
//        System.out.println("中序遍历:");
//        binaryTree.infixOrder();
//        System.out.println("后序遍历:");
//        binaryTree.postOrder();    
    }
}
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("树为空,无法遍历!");
        }
    }

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

二叉树查找指定结点

思路分析
  • 前序查找思路:

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

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

    1. 判断当前结点的左子结点是否为空,如果不为空,则递归后续查找。
    2. 如果找到,就返回,如果没有找到,就判断当前结点的右子结点是否为空。如果不为空,则右递归进行后续查找,如果找到,就返回。
    3. 若上述未找到,就和当前结点进行比较,此时如果找到则返回,否则返回null。
    代码实现:
    package cn.ysk.tree;
    
    public class BinaryTreeDemo {
        public static void main(String[] args) {
            //先需要创建一颗二叉树
            BinaryTree binaryTree = new BinaryTree();
            //创建需要的结点
            HeroNode root = new HeroNode(1, "宋江");
            HeroNode node2 = new HeroNode(2, "吴用");
            HeroNode node3 = new HeroNode(3, "卢俊义");
            HeroNode node4 = new HeroNode(4, "林冲");
            HeroNode node5 = new HeroNode(5, "关胜");
            //手动创建二叉树
            binaryTree.setRoot(root);
            root.setLeft(node2);
            root.setRight(node3);
            node3.setRight(node4);
            node3.setLeft(node5);
    
    //        System.out.println("前序遍历方式~~~");
    //		HeroNode resNode = binaryTree.preOrderSearch(6);
    //		if (resNode != null) {
    //			System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
    //		} else {
    //			System.out.printf("没有找到 no = %d 的英雄", 6);
    //		}
    
    //        System.out.println("中序遍历方式~~~");
    //        HeroNode resNode = binaryTree.infixOrderSearch(5);
    //        if (resNode != null) {
    //            System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
    //        } else {
    //            System.out.printf("没有找到 no = %d 的英雄", 2);
    //        }
    //
    //        System.out.println("后序遍历方式~~~");
    //        HeroNode 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);
    //        }
    //    }
           
        }
    }
    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) {      //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 this.root.postOrderSearch(no);
            }else {
                return null;
            }
        }
    }
    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) {
           if(this.no == no) { //比较当前结点是不是
               return this;
           }
            //1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
            //2.如果左递归前序查找,找到结点,则返回
           HeroNode resNode = null;
           if(this.left != null) {
               resNode = this.left.preOrderSearch(no);
           }
           if(resNode != null) {  //说明左子树找到
               return resNode;
           }
            //1.左递归前序查找,找到结点,则返回,否继续判断,
            //2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
           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;
            }
            //如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
            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.preOrderSearch(no);
            }
            if(resNode != null) {
                return resNode;
            }
            //如果左右子树都没有找到,就比较当前结点是不是
            if(this.no == no) {
                return this;
            }
            return resNode;
        }
    }    
    

二叉树删除节点

简单介绍与思路分析

要求:

  • 如果删除的节点是叶子节点,则删除该节点
  • 如果删除的节点是非叶子节点,则删除该子树

思路:

首先考虑根节点root,如果只有一个节点root,则等价二叉树置空

  1. 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否是要删除的结点,而不能去判断当前这个结点是不是需要被删除.
  2. 如果当前结点的左子结点不为空,并且左子结点就是要删除的结点,就将this.left=null,并且直接返回(结束递归删除)
  3. 如果当前结点的右子结点不为空,并且右子结点就是要删除的结点,就将this.right=null;并且直接返回(结束递归删除)
  4. 如果第2和第3步没有删除结点,那么我们就需要向左子树进行递归删除
  5. 如果第4步也没有删除结点,则应当向右子树进行递归删除.
代码实现:

附上 二叉树的遍历,查找,删除的全部代码:

package cn.ysk.tree;

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

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

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

//        System.out.println("中序遍历方式~~~");
//        HeroNode resNode = binaryTree.infixOrderSearch(5);
//        if (resNode != null) {
//            System.out.printf("找到了,信息为 no=%d name=%s", resNode.getNo(), resNode.getName());
//        } else {
//            System.out.printf("没有找到 no = %d 的英雄", 2);
//        }
//
        System.out.println("后序遍历方式~~~");
        HeroNode resNode = binaryTree.postOrderSearch(2);
        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();
    }
}
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) {      //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 this.root.postOrderSearch(no);
        }else {
            return null;
        }
    }

    public void delNode(int no) {
        if(root != null) {
            if(root.getNo() == no) {
                root = null;
            }else {
                root.delNode(no);
            }
        }else {
            System.out.println("空树,不能删除!");
        }
    }
}
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) {
       if(this.no == no) { //比较当前结点是不是
           return this;
       }
        //1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
        //2.如果左递归前序查找,找到结点,则返回
       HeroNode resNode = null;
       if(this.left != null) {
           resNode = this.left.preOrderSearch(no);
       }
       if(resNode != null) {  //说明左子树找到
           return resNode;
       }
        //1.左递归前序查找,找到结点,则返回,否继续判断,
        //2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
       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;
        }
        //如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
        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.preOrderSearch(no);
        }
        if(resNode != null) {
            return resNode;
        }
        //如果左右子树都没有找到,就比较当前结点是不是
        if(this.no == no) {
            return this;
        }
        return resNode;
    }

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

		 */
        //2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将this.left = null; 并且就返回(结束递归删除)
        if(this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        //3.如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将this.right= null ;并且就返回(结束递归删除)
        if(this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        //4.我们就需要向左子树进行递归删除
        if(this.left != null) {
            this.left.delNode(no);
        }
        //5.则应当向右子树进行递归删除
        if(this.right != null) {
            this.right.delNode(no);
        }
    }
}

顺序存储二叉树

顺序存储二叉树的概念

基本说明:

从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,看下面的示意图。

在这里插入图片描述

要求:

  • 上图的二叉树的结点,要求以数组的方式来存放arr:[1,2,3,4,5,6,6]
  • 要求在遍历数组arr时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成结点的遍历

顺序存储二叉树的特点:

  • 顺序二叉树通常只考虑完全二叉树
  • 第n个元素的左子节点为2*n+1
  • 第n个元素的右子节点为2*n+2
  • 第n个元素的父节点为(n-1)/2
  • n:表示二叉树中的第几个元素(按0开始编号如图所示)

顺序存储二叉树遍历

下面代码包括前序、中序、后序遍历:

package cn.ysk.tree;

public class ArrBinaryTreeDemo {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6,7};
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
        System.out.println("前序遍历的方式:");
        arrBinaryTree.preOrder();
        System.out.println();
        System.out.println("中序遍历的方式:");
        arrBinaryTree.infixsOrder();
        System.out.println();
        System.out.println("后序遍历的方式:");
        arrBinaryTree.postOrder();
    }
}
class ArrBinaryTree {
    private int[] arr;
    public ArrBinaryTree(int[] arr) {
        this.arr = arr;
    }

    public void preOrder() { //简化代码
        this.preOrder(0);
    }

    public void infixsOrder() {
        this.infixOrder(0);
    }

    public void postOrder() {
        this.postOrder(0);
    }

    //前序遍历
    public void preOrder(int index) { //1 2 4 5 3 6 7
        if(arr.length == 0 || arr == null) {
            System.out.println("数组为空,不能按照二叉树的前序遍历!");
        }
        System.out.print(arr[index]);
        //左递归遍历
        if(2*index + 1 < arr.length) {
            preOrder(2*index + 1);
        }
        //右递归遍历
        if(2*index + 2 < arr.length) {
            preOrder(2*index + 2);
        }
    }

    //中序遍历
    public void infixOrder(int index) { //1 2 4 5 3 6 7
        if(arr.length == 0 || arr == null) {
            System.out.println("数组为空,不能按照二叉树的中序遍历!");
        }
        if(2*index + 1 < arr.length) {
            infixOrder(2*index + 1);
        }
        System.out.print(arr[index]);
        if(2*index + 2 < arr.length) {
            infixOrder(2*index + 2);
        }
    }

    //后序遍历
    public void postOrder(int index) { //1 2 4 5 3 6 7
        if(arr.length == 0 || arr == null) {
            System.out.println("数组为空,不能按照二叉树的后序遍历!");
        }
        if(2*index + 1 < arr.length) {
            postOrder(2*index + 1);
        }
        if(2*index + 2 < arr.length) {
            postOrder(2*index + 2);
        }
        System.out.print(arr[index]);
    }
}

线索化二叉树

线索化二叉树的引入

在这里插入图片描述

  1. 当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }
  2. 当以二叉链表作为存储结构时,只存储了结点的左右孩子信息,而没有存储结点在遍历序列中的前驱和后继信息。要想得到,最好的方式采用:充分利用二叉链表中的空链域,将遍历过程中结点的前驱、后继信息保存下来。
  3. 在一颗有n个结点的二叉树中,共有2n个指针域,其中n+1(2n-(n-1),共2n个指针,除根结点外每个用掉一个)个空指针域,可以利用这些空指针域来指示其前驱和后继。
  4. 利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附在这里插入图片描述
    加的指针称为**“线索”**)
  5. 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为**线索二叉树(**ThreadedBinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种

线索二叉树应用案例(线索化+遍历)

  • 线索化

说明:当线索化二叉树后,Node节点的属性left和right,有如下情况:

  • left指向的是左子树,也可能是指向的前驱节点.比如①节点left指向的左子树,而⑩节点的left指向的就是前驱节点.
  • right指向的是右子树,也可能是指向后继节点,比如①节点right指向的是右子树,而⑩节点的right指向的是后继节点.

为了表示left和right域指向的时左\右孩子还是前驱、后继,可以加两个标志域 leftType、rightType,以明确真正的指向!

在这里插入图片描述

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

二叉树结点结构

代码实现:

package cn.ysk.tree.threadedbinarytree;

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

        //二叉树,后面我们要递归创建, 现在简单处理使用手动创建
        root.setLeft(node2);
        root.setRight(node3);
        node2.setLeft(node4);
        node2.setRight(node5);
        node3.setLeft(node6);

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

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

        System.out.println("使用线索化的方式遍历 线索化二叉树");
        threadedBinaryTree.threadedList();
    }
}
//定义ThreadedBinaryTree 实现了线索化功能的二叉树
class ThreadedBinaryTree {
    private HeroNode root;  //根节点

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

    public void setRoot(HeroNode root) {
        this.root = root;
    }
	
     //遍历线索化二叉树的方法(中序)
    public void threadedList() {
        HeroNode node = root;
        while (node != null) {
			//首先要明确中序遍历的思想,左右根
            //循环的找到leftType == 1的结点,第一个找到就是8结点
            //后面随着遍历而变化,因为当leftType==1时,说明该结点是按照线索化
            //处理后的有效结点
            while(node.getLeftType() == 0) {
                node = node.getLeft();
            }
            System.out.println(node);
            while (node.getRightType() == 1) {
                node = node.getRight();
                System.out.println(node);
            }
            //替换这个遍历的结点
            node = node.getRight();
        }
    }
    
    //编写对二叉树进行中序线索化的方法
    public void  threadedNodes(HeroNode node) {
        if(node == null) {
            return;
        }
        //(一)先线索化左子树
        threadedNodes(node.getLeft());

        //(二)线索化当前结点[有难度]
        //处理当前结点的前驱结点
        //以8结点来理解
        //8结点的.left = null , 8结点的.leftType = 1
        if(node.getLeft() == null) { //这个点相当于中序遍历的最左边,所以没有前驱结点
            //让当前结点的左指针指向前驱结点(8->null)
            node.setLeft(pre);  //此时pre为null
            //修改当前结点的左指针的类型,指向前驱结点
            node.setLeftType(1);
        }

        //处理后继结点
        if(pre != null && pre.getRight() == null) {  //二叉树是单向的,要通过这种方式来完成,这里做的操作相当于是8->3
            //让前驱结点的右指针指向当前结点
            pre.setRight(node);
            //修改前驱结点的右指针类型
            pre.setRightType(1);
        }
        //!!! 每处理一个结点后,让当前结点是下一个结点的前驱结点
        pre = node;
        //(三)在线索化右子树
        threadedNodes(node.getRight());
    }
  
}
class HeroNode {
    private int no;
    private String name;
    private HeroNode left;
    private HeroNode right;
    //说明
    //1. 如果leftType == 0 表示指向的是左子树, 如果 1 则表示指向前驱结点
    //2. 如果rightType == 0 表示指向是右子树, 如果 1表示指向后继结点
    private int leftType;
    private int rightType;

    public int getLeftType() {
        return leftType;
    }

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

    public int getRightType() {
        return rightType;
    }

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

    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

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

    public String getName() {
        return name;
    }

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

    public HeroNode getLeft() {
        return left;
    }

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

    public HeroNode getRight() {
        return right;
    }

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

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

第10章 树结构的实际应用

堆排序

堆排序的基本介绍

  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。

  2. 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系。

  3. 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆

  4. 一般升序采用大顶堆,降序采用小顶堆

  5. 大顶堆举例说明:

在这里插入图片描述

  1. 小顶堆举例说明:

在这里插入图片描述

堆排序的基本思想

堆排序共分为两大过程:构建堆和堆排序

  • 构建堆

    将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点

  • 将根节点与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造大顶堆,这样会得到剩余元素的最大值。再将n-2个元素重新构造大顶堆……如此反复执行,便能得到一个有序序列了。

注意:构建堆的过程是自底到顶的和自顶到底两个过程,而堆排序的过程是从根节点为根基构造大顶堆(之前已经将大的元素放在了上方,为了方便,直接从根节点开始)

堆排序好的博客:

关于堆排序的几个问题

  1. 为什么最后一个非叶子结点的序号是n/2 - 1 ?(n=arr.length-1)

    完全二叉树的性质之一是:如果节点序号为i,在它的左孩子序号为2 * i+1,右孩子序号为2*i+2。

    可以分两种情形考虑:

    • 堆的最后一个非叶子节点若只有左孩子

      则n-1=2*i+1,推出i=n/2-1

    • 堆的最后一个非叶子节点有左右两个孩子

      左孩子的序号为n-2,在n-2=2 * i+1,推出i=(n-1)/2-1;右孩子的序号为n-1,则n-1=2*i+2,推出i=(n-1)/2-1

    很显然,当完全二叉树最后一个节点是其父节点的左孩子时,树的节点数为偶数;当完全二叉树最后一个节点是其父节点的右孩子时,树的节点数为奇数

    根据java语法的特征,整数除不尽时向下取整,则若n为奇数时(n-1)/2-1=n/2-1。

    因此对于非叶子结点有左右两个孩子时最后一个非叶子节点的序号也是n/2-1

    上述过程来源于:

  2. 为什么构建大顶堆要先从下往上构建,而不是从上往下?

    根据大顶堆的性质,每个节点的值都大于或者等于它的左右子节点的值。所以我们需要找到所有包含子节点的节点,也就是非叶子节点,然后调整他们的父子关系,非叶子节点遍历的顺序应该是从下往上这比从上往下的顺序遍历次数少很多,因为,大顶堆的性质要求父节点的值要大于或者等于子节点的值,如果从上往下遍历,当某个节点即是父节点又是子节点并且它的子节点仍然有子节点的时候,因为子节点还没有遍历到,所以子节点不符合大顶堆性质,当子节点调整后,必然会影响其父节点需要二次调整。但是从下往上的方式不需要考虑父节点,因为当前节点调整完之后,当前节点必然比它的所有子节点都大,所以,只会影响到子节点二次调整。相比之下,从下往上的遍历方式比从上往下的方式少了父节点的二次调整。

    上述过程来源于:

  3. 堆排序不稳定。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

    例如:8 5 9 4 1 7(1) 6 7(2),从第一个非叶子结点4开始构建堆的时候,7(2)会往上移动到5的地方,而7(1)始终在那个位置没动,此时就在7(1)的前面了,丧失了稳定性。

堆排序的代码实现

package cn.ysk.sort;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class HeapSort {
 public static void main(String[] args) {
     //要求将数组进行升序排序
//        int arr[] = {4, 6, 8, 5, 9};
//        heapSort(arr);
     getTime();
 }

 public static void heapSort(int arr[]) {
     //分步完成
//		adjustHeap2(arr, 1, arr.length);
//		System.out.println("第一次" + Arrays.toString(arr)); // 4, 9, 8, 5, 6
     int temp = 0;
     //构建大顶堆
     for (int i = (arr.length-1) / 2; i >=0 ; i--) { //从下往上构建
         adjustHeap(arr, i, arr.length);
     }
     //进行堆排序
     for (int j = arr.length - 1; j > 0 ; j--) { //这里类似于冒泡,到最后一个就不需要交换了
         temp = arr[0];
         arr[0] = arr[j];
         arr[j] = temp;
         adjustHeap(arr, 0, j); //在此基础上,重新构建堆,从上往下构建(上面构建大顶堆时,大的数据已经在上方)
     }
//        System.out.println("数组arr=" + Arrays.toString(arr));
 }

 //将一个数组(二叉树), 调整成一个大顶堆
 /**
  * 功能: 以索引为i的位置的非叶子节点,调整其下所有的子树,构成一个堆的结构
  * 举例  int arr[] = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => 得到 {4, 9, 8, 5, 6}
  * 如果我们再次调用  adjustHeap 传入的是 i = 0 => 得到 {4, 9, 8, 5, 6} => {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];
     //为什么要循环?
     //如果此时i是根节点,但是根节点又是一个极小的值,那么它就需要与它的左或右结点交换,交换了之后,根节点的
     //左或右结点的值变得非常小,于是又要像之前一样进行构建堆(在这个过程中,始终保持堆这种数据结构的特性!)
     for (int k = 2*i +1; k < length; k = 2*k + 1) {
         if(k + 1 < length && arr[k] < arr[k+1]) { //右子结点比左子结点大,那么k指向右子节点,k+1>length就没必要比较了
             k++;
         }
         if(arr[k] > temp) {
             //父节点i位置的值比左右子节点小,那么需要调整
             arr[i] = arr[k];
             i = k; //!!! i 指向 k,继续循环比较
         }else {
             break;
         }
     }
     //当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)
     arr[i] = temp;
 }

 public static void getTime(){   //运行80000个数据耗时:9ms
     int[] arr = new int[80000];
     for (int i = 0; i < 80000; i++) {
         arr[i] = (int) (Math.random()*80000);
     }
     long startTime = System.currentTimeMillis();
     heapSort(arr);
     long endTime = System.currentTimeMillis();
//        System.out.println(Arrays.toString(arr));
     System.out.println("运行"+arr.length+"个数据耗时:"+(endTime-startTime)+"ms");
 }
}

哈夫曼树

基本介绍

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

哈夫曼树几个重要概念和举例说明

  1. 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1
  2. 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
  3. 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weightedpathlength),权值越大的结点离根结点越近的二叉树才是最优二叉树。
  4. WPL最小的就是赫夫曼树

哈夫曼树创建思路图解

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

步骤:

  1. 从小到大进行排序,将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树

  2. 取出根节点权值最小的两颗二叉树

  3. 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和

  4. 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

  5. 图解:

在这里插入图片描述

哈夫曼树的代码实现

package cn.ysk.huffmantree;

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

public class HuffmanTree {
    public static void main(String[] args) {
        int[] arr = {13,7,8,3,29,6,1};
        Node root = creatHuffmanTree(arr);
        preOrder(root);
    }

    public static void preOrder(Node root) {
        if(root != null) {
            root.preOrder();
        }else {
            System.out.println("空树,无法遍历!");
        }
    }

    public static Node creatHuffmanTree(int[] arr) {
        List<Node> list = new ArrayList<>();
        for (int value: arr) {
            list.add(new Node(value));
        }
        while (list.size() > 1) {
            Collections.sort(list);
            Node leftNode = list.get(0); //选出两个最小的,加上他们的和,组成二叉树
            Node rightNode = list.get(1);
            //创建他们两个的父节点,三者构成新的二叉树
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;
            //删除这两个结点,并将父节点的值加入到集合中
            list.remove(leftNode);
            list.remove(rightNode);
            list.add(parent);
        }
        return list.get(0); //最后集合中中只剩下一个元素,那就是根节点!
    }
}
class Node implements Comparable<Node>{
    public int value;
    public Node left;
    public Node right;

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

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

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

    @Override
    public int compareTo(Node o) {
        return this.value - o.value; //小的放在前面,this在前就是升序
    }
}

哈夫曼编码

基本介绍

  1. 赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
  2. 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
  3. 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
  4. 赫夫曼码是可变字长编码(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 // 各个字符对应的个数
    按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值。

    赫夫曼编码的简易步骤:

    1. 创建赫夫曼树
    2. 根据赫夫曼树给各个字符,规定编码(前缀编码)
    3. 根据哈夫曼编码得到字符串对应的编码(注意:我们这里使用无损压缩)

创建哈夫曼树

思路分析

传输的字符串:

i like like like java do you like a java

d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9//各个字符对应的个数

按照上面字符出现的次数构建一颗赫夫曼树**,次数**作为权值。

  • 从小到大进行排序,将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树

  • 取出根节点权值最小的两颗二叉树

  • 组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和

  • 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

  • 注意,这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl是一样的,都是最小的,最后生成的赫夫曼编码的长度是一样,比如:如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:

在这里插入图片描述

代码实现:
package cn.ysk.huffmancode;

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

public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] bytes = content.getBytes();
        List<Node> nodes = getNodes(bytes);
        Node root = createHuffmanTree(nodes);
    } 
    //前序遍历的方法
    private static void preOrder(Node root) {
        if(root != null) {
            root.preOrder();
        }else {
            System.out.println("赫夫曼树为空");
        }
    }

    public static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            Collections.sort(nodes);
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            //注意:在这里父节点是新建立的结点,并不是原有的结点,所以没有对于的data
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parent);
        }
        return nodes.get(0);
    }

    /**
     * @param bytes 接收的字节数组
     * @return      返回的是它的list形式,为后续构建树做准备
     */
    public static List<Node> getNodes(byte[] bytes) {
        List<Node> nodes = new ArrayList<>();
        HashMap<Byte, Integer> map = new HashMap<>();
        for (byte b: bytes) {  //增强for可以遍历到byte数组中每个元素的ASCLL值
            Integer count = map.get(b);
            if(count ==null ) {
                map.put(b, 1);
            }else {
                map.put(b, count+1);
            }
        }
        //把每一个键值对转成一个Node 对象,并加入到nodes集合
        //遍历map
        Set<Byte> keySet = map.keySet();
        for (Byte key : keySet) {
            nodes.add(new Node(key,map.get(key)));
        }
        return nodes;
    }
}
class Node implements Comparable<Node>{
    Byte data; //存放的是数据本身,比如'a' => 97 ' ' => 32
    int weight; //存放的是权值,指的是每个字符出现的次数
    Node left;
    Node right;

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

    @Override
    public String toString() {
        return "Node{" +
                "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();
        }
    }

    @Override
    public int compareTo(Node o) {
        return this.weight - o.weight;  //升序排列
    }
}

生成赫夫曼编码和赫夫曼编码后的数据

基本介绍
  1. 根据赫夫曼树,给各个字符,规定编码(前缀编码),向左的路径为0向右的路径为1,编码如下:

    o: 1000 u: 10010 d: 100110 y: 100111 i: 101
    a : 110 k: 1110 e: 1111 j: 0000 v: 0001
    l: 001 : 01

  2. 使用赫夫曼编码来生成赫夫曼编码数据,即按照上面的赫夫曼编码,将"ilikelikelikejavadoyoulikeajava"字符串生成对应的编码数据,形式如下:

    1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

代码实现
//为了调用方便,我们重载 getCodes
    private static Map<Byte, String> getCodes(Node root) {
        if(root == null) {
            return null;
        }
       getCodes(root, "", stringBuilder);
        return huffmanCodes;
    }

    //生成赫夫曼树对应的赫夫曼编码
    //思路:
    //1. 将赫夫曼编码表存放在 Map<Byte,String> 形式
    //   生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
    static Map<Byte, String> huffmanCodes = new HashMap<Byte,String>();
    //2. 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();

    /**
     * 将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
     * @param node  传入结点
     * @param code  路径: 左子结点是 0, 右子结点 1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(Node node,String code,StringBuilder stringBuilder) {
        //这里为何要再new? 为了递归回溯
        //如果不new,stringBuilder一直在变化,例如向左一直到00,在append0.就是000,接下来回溯,向左走,带进去的是000,而不是00!
        StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
//        StringBuilder stringBuilder1 = stringBuilder; //直接赋值不行!!!,两个还是会跟着一起变化
        stringBuilder1.append(code);
        if(node != null) {
            if(node.data == null) { //非叶子结点
                getCodes(node.left, "0", stringBuilder1); //左0右1,向左递归
                getCodes(node.right, "1", stringBuilder1);//向右递归
            }else {
                huffmanCodes.put(node.data, stringBuilder1.toString());
            }
        }
    }

将原byte数组进行压缩

基本介绍

原字符数组:[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]

压缩后的字符数组:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

代码实现
//使用一个方法,将前面的方法封装起来,便于我们的调用.
    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        List<Node> nodes = getNodes(bytes);
        //根据 nodes 创建的哈夫曼树
        Node root = createHuffmanTree(nodes);
        //根据哈夫曼树得到每个字母的哈夫曼编码
        Map<Byte, String>  huffmanCodes = getCodes(root);
//        将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
        byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
        return huffmanCodeBytes;
    }

    //编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
    /**
     * @param bytes 这时原始的字符串对应的 byte[]
     * @param huffmanCodes  生成的赫夫曼编码map
     * @return  返回赫夫曼编码处理后的 byte[]
     * 举例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
     * 返回的是 字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * => 对应的 byte[] huffmanCodeBytes  ,即 8位对应一个 byte,放入到 huffmanCodeBytes
     * huffmanCodeBytes[0] =  10101000(补码) => byte  [推导  10101000=> 10101000 - 1 => 10100111(反码)=> 11011000= -88 ]
     * huffmanCodeBytes[1] = -88
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b: bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        int len;
        //也可以写成
        //int len = (StringBuilder.lenth() + 7) / 8;
        if(stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        }else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建 存储压缩后的 byte数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;
        for (int i = 0; i < stringBuilder.length(); i+=8) {
            String strByte;
            if(i + 8 > stringBuilder.length()) {
                strByte = stringBuilder.substring(i);
            }else {
                strByte = stringBuilder.substring(i,i+8); //左闭右开
            }
//            System.out.println(strByte); 10101000
            //将strByte 转成一个byte,放入到 huffmanCodeBytes
            huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

使用赫夫曼编码解码

基本介绍

使用赫夫曼编码来解码数据,具体要求是:

前面我们得到了赫夫曼编码和对应的编码byte[],即:[-88,-65,-56,-65,-56,-65,-55,77,-57,6,-24,-14,-117,-4,-60,-90,28]。

现在要求使用赫夫曼编码,进行解码,又重新得到原来的字符串"ilikelikelikejavadoyoulikeajava"

解码过程,就是编码的一个逆向操作。

代码实现
//完成数据的解压
    //思路
    //1. 将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
    //   重写先转成 赫夫曼编码对应的二进制的字符串 "1010100010111..."
    //2.  赫夫曼编码对应的二进制的字符串 "1010100010111..." =》 对照 赫夫曼编码  =》 "i like like like java do you like a java"
    //编写一个方法,完成对压缩数据的解码
    /**
     *
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 就是原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes) {
        //1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
        StringBuilder sb = new StringBuilder();
        //将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判断是否是最后一个元素,是就返回真
            boolean flag = (i == huffmanBytes.length -1);
            String s = byteToBitString(!flag, b);
            sb.append(s);
        }
        //把字符串安装指定的赫夫曼编码进行解码
        //把赫夫曼编码表进行调换,因为反向查询 之前是a->100 现在通过100->a
        //之前通过byte找到String,现在返过来,通过String找到byte
        Map<String,Byte> map = new HashMap<>();
        Set<Byte> bytes = huffmanCodes.keySet();
        for (Byte aByte : bytes) {
            map.put(huffmanCodes.get(aByte), aByte);
        }
        //创建要给集合,存放byte
        List<Byte> list = new ArrayList<>();
        //扫描StringBuilder,一旦匹配到就加入到list集合中
        for (int i = 0; i < sb.length();) {
            int count = 1;
            boolean flag = true;
            Byte b = null;
            String key;
            while (flag) {
                key = sb.substring(i, i+count);
                b = map.get(key);
                if(b == null) {
                    count++; //+1继续扫描
                }else {
                    flag = false;  //匹配到,跳出循环
                }
            }
            list.add(b);
            i += count; //在这里,去掉了i++,可以自己举例验证,若i++,则多出了一个索引!
        }
        //当for循环结束后,我们list中就存放了所有的字符  "i like like like java do you like a java"
        //把list 中的数据放入到byte[] 并返回
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    /**
     * 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
     * @param b 传入的 byte
     * @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
     * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
     */
    private static String byteToBitString(boolean flag,byte b ) {
        //使用变量保存 b
        int temp = b; //将 b 转成 int
        //如果是正数我们还存在补高位
        if(flag) {
            temp |= 256; //按位或 256  1 0000 0000  | 0000 0001 => 1 0000 0001
        }
        String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
        if(flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }

文件压缩

基本介绍

我们学习了通过赫夫曼编码对一个字符串进行编码和解码,下面我们来完成对文件的压缩和解压,具体要求:给你一个图片文件,要求对其进行无损压缩,看看压缩效果如何。

思路:读取文件->得到赫夫曼编码表->完成压缩

代码实现
	//编写方法,将一个文件进行压缩
    /**
     *
     * @param srcFile 你传入的希望压缩的文件的全路径
     * @param dstFile 我们压缩后将压缩文件放到哪个目录
     */
    public static void zipFile(String srcFile, String dstFile) {

        //创建输出流
        OutputStream os = null;
        ObjectOutputStream oos = null;
        //创建文件的输入流
        FileInputStream is = null;
        try {
            //创建文件的输入流
            is = new FileInputStream(srcFile);
            //创建一个和源文件大小一样的byte[]
            byte[] b = new byte[is.available()];
            //读取文件
            is.read(b);
            //直接对源文件压缩
            byte[] huffmanBytes = huffmanZip(b);
            //创建文件的输出流, 存放压缩文件
            os = new FileOutputStream(dstFile);
            //创建一个和文件输出流关联的ObjectOutputStream
            oos = new ObjectOutputStream(os);
            //把 赫夫曼编码后的字节数组写入压缩文件
            oos.writeObject(huffmanBytes); //我们是把
            //这里我们以对象流的方式写入 赫夫曼编码,是为了以后我们恢复源文件时使用
            //注意一定要把赫夫曼编码 写入压缩文件
            oos.writeObject(huffmanCodes);


        }catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        }finally {
            try {
                is.close();
                oos.close();
                os.close();
            }catch (Exception e) {
                // TODO: handle exception
                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数组  huffmanBytes
            byte[] huffmanBytes = (byte[])ois.readObject();
            //读取赫夫曼编码表
            Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();

            //解码
            byte[] bytes = decode(huffmanCodes, huffmanBytes);
            //将bytes 数组写入到目标文件
            os = new FileOutputStream(dstFile);
            //写数据到 dstFile 文件
            os.write(bytes);
        } catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        } finally {

            try {
                os.close();
                ois.close();
                is.close();
            } catch (Exception e2) {
                // TODO: handle exception
                System.out.println(e2.getMessage());
            }

        }
    }

以上所有代码汇总

package cn.ysk.huffmancode;



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

public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] bytes = content.getBytes();
        System.out.println(Arrays.toString(bytes));
        List<Node> nodes = getNodes(bytes);
        Node root = createHuffmanTree(nodes);
//        preOrder(root);
//        getCodes(root, "", stringBuilder);
//        Map<Byte, String> huffmanCodes = getCodes(root);
//        System.out.println("生成的编码:" + huffmanCodes);
        byte[] b = huffmanZip(bytes);
        System.out.println(Arrays.toString(b));
//        byte[] b = huffmanZip(bytes);
//        System.out.println(Arrays.toString(b));
//        byte[] decodeByte = decode(huffmanCodes, b);
//        System.out.println(new String(decodeByte));
        //测试压缩文件
//		String srcFile = "F://src.bmp";
//		String dstFile = "F://dst.zip";
//
//		zipFile(srcFile, dstFile);
//		System.out.println("压缩文件ok~~");
        //测试解压文件
//        String zipFile = "f://dst.zip";
//        String dstFile = "f://src2.bmp";
//        unZipFile(zipFile, dstFile);
//        System.out.println("解压成功!");

    }

    //编写一个方法,完成对压缩文件的解压
    /**
     *
     * @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数组  huffmanBytes
            byte[] huffmanBytes = (byte[])ois.readObject();
            //读取赫夫曼编码表
            Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();

            //解码
            byte[] bytes = decode(huffmanCodes, huffmanBytes);
            //将bytes 数组写入到目标文件
            os = new FileOutputStream(dstFile);
            //写数据到 dstFile 文件
            os.write(bytes);
        } catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        } finally {

            try {
                os.close();
                ois.close();
                is.close();
            } catch (Exception e2) {
                // TODO: handle exception
                System.out.println(e2.getMessage());
            }

        }
    }
    //编写方法,将一个文件进行压缩
    /**
     *
     * @param srcFile 你传入的希望压缩的文件的全路径
     * @param dstFile 我们压缩后将压缩文件放到哪个目录
     */
    public static void zipFile(String srcFile, String dstFile) {

        //创建输出流
        OutputStream os = null;
        ObjectOutputStream oos = null;
        //创建文件的输入流
        FileInputStream is = null;
        try {
            //创建文件的输入流
            is = new FileInputStream(srcFile);
            //创建一个和源文件大小一样的byte[]
            byte[] b = new byte[is.available()];
            //读取文件
            is.read(b);
            //直接对源文件压缩
            byte[] huffmanBytes = huffmanZip(b);
            //创建文件的输出流, 存放压缩文件
            os = new FileOutputStream(dstFile);
            //创建一个和文件输出流关联的ObjectOutputStream
            oos = new ObjectOutputStream(os);
            //把 赫夫曼编码后的字节数组写入压缩文件
            oos.writeObject(huffmanBytes); //我们是把
            //这里我们以对象流的方式写入 赫夫曼编码,是为了以后我们恢复源文件时使用
            //注意一定要把赫夫曼编码 写入压缩文件
            oos.writeObject(huffmanCodes);


        }catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        }finally {
            try {
                is.close();
                oos.close();
                os.close();
            }catch (Exception e) {
                // TODO: handle exception
                System.out.println(e.getMessage());
            }
        }

    }

    //完成数据的解压
    //思路
    //1. 将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
    //   重写先转成 赫夫曼编码对应的二进制的字符串 "1010100010111..."
    //2.  赫夫曼编码对应的二进制的字符串 "1010100010111..." =》 对照 赫夫曼编码  =》 "i like like like java do you like a java"
    //编写一个方法,完成对压缩数据的解码
    /**
     *
     * @param huffmanCodes 赫夫曼编码表 map
     * @param huffmanBytes 赫夫曼编码得到的字节数组
     * @return 就是原来的字符串对应的数组
     */
    private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes) {
        //1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
        StringBuilder sb = new StringBuilder();
        //将byte数组转成二进制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判断是否是最后一个元素,是就返回真
            boolean flag = (i == huffmanBytes.length -1);
            String s = byteToBitString(!flag, b);
            sb.append(s);
        }
        //把字符串安装指定的赫夫曼编码进行解码
        //把赫夫曼编码表进行调换,因为反向查询 之前是a->100 现在通过100->a
        //之前通过byte找到String,现在返过来,通过String找到byte
        Map<String,Byte> map = new HashMap<>();
        Set<Byte> bytes = huffmanCodes.keySet();
        for (Byte aByte : bytes) {
            map.put(huffmanCodes.get(aByte), aByte);
        }
        //创建要给集合,存放byte
        List<Byte> list = new ArrayList<>();
        //扫描StringBuilder,一旦匹配到就加入到list集合中
        for (int i = 0; i < sb.length();) {
            int count = 1;
            boolean flag = true;
            Byte b = null;
            String key;
            while (flag) {
                key = sb.substring(i, i+count);
                b = map.get(key);
                if(b == null) {
                    count++; //+1继续扫描
                }else {
                    flag = false;  //匹配到,跳出循环
                }
            }
            list.add(b);
            i += count; //在这里,去掉了i++,可以自己举例验证,若i++,则多出了一个索引!
        }
        //当for循环结束后,我们list中就存放了所有的字符  "i like like like java do you like a java"
        //把list 中的数据放入到byte[] 并返回
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    /**
     * 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
     * @param b 传入的 byte
     * @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
     * @return 是该b 对应的二进制的字符串,(注意是按补码返回)
     */
    private static String byteToBitString(boolean flag,byte b ) {
        //使用变量保存 b
        int temp = b; //将 b 转成 int
        //如果是正数我们还存在补高位
        if(flag) {
            temp |= 256; //按位或 256  1 0000 0000  | 0000 0001 => 1 0000 0001
        }
        String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
        if(flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }

    //使用一个方法,将前面的方法封装起来,便于我们的调用.
    /**
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        List<Node> nodes = getNodes(bytes);
        //根据 nodes 创建的哈夫曼树
        Node root = createHuffmanTree(nodes);
        //根据哈夫曼树得到每个字母的哈夫曼编码
        Map<Byte, String>  huffmanCodes = getCodes(root);
//        将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
        byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
        return huffmanCodeBytes;
    }

    //编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
    /**
     * @param bytes 这时原始的字符串对应的 byte[]
     * @param huffmanCodes  生成的赫夫曼编码map
     * @return  返回赫夫曼编码处理后的 byte[]
     * 举例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
     * 返回的是 字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * => 对应的 byte[] huffmanCodeBytes  ,即 8位对应一个 byte,放入到 huffmanCodeBytes
     * huffmanCodeBytes[0] =  10101000(补码) => byte  [推导  10101000=> 10101000 - 1 => 10100111(反码)=> 11011000= -88 ]
     * huffmanCodeBytes[1] = -88
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b: bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        int len;
        //也可以写成
        //int len = (StringBuilder.lenth() + 7) / 8;
        if(stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        }else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建 存储压缩后的 byte数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;
        for (int i = 0; i < stringBuilder.length(); i+=8) {
            String strByte;
            if(i + 8 > stringBuilder.length()) {
                strByte = stringBuilder.substring(i);
            }else {
                strByte = stringBuilder.substring(i,i+8); //左闭右开
            }
//            System.out.println(strByte); 10101000
            //将strByte 转成一个byte,放入到 huffmanCodeBytes
            huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

    //为了调用方便,我们重载 getCodes
    private static Map<Byte, String> getCodes(Node root) {
        if(root == null) {
            return null;
        }
       getCodes(root, "", stringBuilder);
        return huffmanCodes;
    }

    //生成赫夫曼树对应的赫夫曼编码
    //思路:
    //1. 将赫夫曼编码表存放在 Map<Byte,String> 形式
    //   生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
    static Map<Byte, String> huffmanCodes = new HashMap<Byte,String>();
    //2. 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();

    /**
     * 将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
     * @param node  传入结点
     * @param code  路径: 左子结点是 0, 右子结点 1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCodes(Node node,String code,StringBuilder stringBuilder) {
        //这里为何要再new? 为了递归回溯
        //如果不new,stringBuilder一直在变化,例如向左一直到00,在append0.就是000,接下来回溯,向左走,带进去的是000,而不是00!
        StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
//        StringBuilder stringBuilder1 = stringBuilder; //直接赋值不行!!!,两个还是会跟着一起变化
        stringBuilder1.append(code);
        if(node != null) {
            if(node.data == null) { //非叶子结点
                getCodes(node.left, "0", stringBuilder1); //左0右1,向左递归
                getCodes(node.right, "1", stringBuilder1);//向右递归
            }else {
                huffmanCodes.put(node.data, stringBuilder1.toString());
            }
        }
    }

    //前序遍历的方法
    private static void preOrder(Node root) {
        if(root != null) {
            root.preOrder();
        }else {
            System.out.println("赫夫曼树为空");
        }
    }

    public static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            Collections.sort(nodes);
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            //注意:在这里父节点是新建立的结点,并不是原有的结点,所以没有对于的data
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parent);
        }
        return nodes.get(0);
    }

    /**
     * @param bytes 接收的字节数组
     * @return      返回的是它的list形式,为后续构建树做准备
     */
    public static List<Node> getNodes(byte[] bytes) {
        List<Node> nodes = new ArrayList<>();
        HashMap<Byte, Integer> map = new HashMap<>();
        for (byte b: bytes) {  //增强for可以遍历到byte数组中每个元素的ASCLL值
            Integer count = map.get(b);
            if(count ==null ) {
                map.put(b, 1);
            }else {
                map.put(b, count+1);
            }
        }
        //把每一个键值对转成一个Node 对象,并加入到nodes集合
        //遍历map
        Set<Byte> keySet = map.keySet();
        for (Byte key : keySet) {
            nodes.add(new Node(key,map.get(key)));
        }
        return nodes;
    }
}
class Node implements Comparable<Node>{
    Byte data; //存放的是数据本身,比如'a' => 97 ' ' => 32
    int weight; //存放的是权值,指的是每个字符出现的次数
    Node left;
    Node right;

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

    @Override
    public String toString() {
        return "Node{" +
                "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();
        }
    }

    @Override
    public int compareTo(Node o) {
        return this.weight - o.weight;  //升序排列
    }
}

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

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

二叉排序树

先看一个需求

给你一个数列 (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) , 创建成对应的二叉排序树为 :

在这里插入图片描述

代码将和二叉排序树的删除在下面一同给出:

二叉排序树的删除

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

  1. 删除叶子节点 (比如:2, 5, 9, 12)
  2. 删除只有一颗子树的节点 (比如:1)
  3. 删除有两颗子树的节点. (比如:7, 3,10 )
二叉排序树结点删除具体分析

在删除时,自身不删除自己,需要借助父节点来完成删除!

  1. 删除叶子结点

    思路过程:

    • 先去找到要删除的结点 targetNode
    • 找到targetNode 的 父结点 parent
    • 确定 targetNode 是 parent的左子结点 还是右子结点
    • 根据前面的情况来对应删除:左子结点 parent.left = null,右子结点 parent.right = null;
  2. 删除只有一颗子树的节点

    思路过程:

    • 先去找到要删除的结点 targetNode

    • 找到targetNode 的 父结点 parent

    • 确定targetNode 的子结点是左子结点还是右子结点

    • targetNode 是 parent 的左子结点还是右子结点

      上述可见,确定待删除结点是父节点的左或者右孩子 + 待删除结点的孩子结点是它的左或者右孩子 ,共2*2四种情况

      下面将这四种情况具体分析:

      注意:在这里可能会遇到一种情况:只剩下一个根节点和一个子树,若待删除结点是这个根结点,则此时它没有父节点,需要单独考虑!

      • 待删除结点有左孩子:

        如果 targetNode 是 parent 的左子结点:parent.left = targetNode.left;

        如果 targetNode 是 parent 的右子结点:parent.right = targetNode.left;

      • 待删除结点有右孩子:

        如果 targetNode 是 parent 的左子结点:parent.left = targetNode.right;

        如果 targetNode 是 parent 的右子结点:parent.right = targetNode.right

  3. 删除有两颗子树的节点.

    思路过程:

    • 需求先去找到要删除的结点 targetNode
    • 找到targetNode 的 父结点 parent
    • 从targetNode 的右子树找到最小的结点(或者:左子树找最大)
    • 用一个临时变量,将 最小结点的值保存 temp =
    • 删除该最小结点
    • targetNode.value = temp

    上面注意:可以右子树找最小,即从待删除结点的右孩子的左子树一直遍历;也可以左子树找最大,即从待删除结点的左孩子的右子树一直遍历!至于为何要这样做,待删除结点有两棵子树并不是直接将目标结点删除,而是根据二叉排序树的特点去找相应的子结点去替换!画图分析即可!

二叉树创建、遍历、删除代码实现

package cn.ysk.binarysorttree;

public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
        BinaryTreeSort binaryTreeSort = new BinaryTreeSort();
        for (int i = 0; i < arr.length; i++) {
            binaryTreeSort.add(new Node(arr[i]));
        }
        System.out.println("中序遍历:");
        binaryTreeSort.infixOrder();
        //测试一下删除叶子结点
        System.out.println("删除节点之后:");
       binaryTreeSort.delNode(1);
       binaryTreeSort.delNode(10);
       binaryTreeSort.delNode(12);
       binaryTreeSort.delNode(9);
       binaryTreeSort.delNode(2);
       binaryTreeSort.delNode(5);
//        System.out.println("123");
        binaryTreeSort.delNode(7);
       binaryTreeSort.delNode(3);
        binaryTreeSort.infixOrder();
    }
}
//创建二叉排序树
class BinaryTreeSort {
    private Node root;

    public Node getRoot() {
        return root;
    }

    public void add(Node node) {
        if(root == null) {
            root = node;
        }else {
            root.add(node);
        }
    }

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

    //找到待删除结点
    public Node searchTargetNode(int value) {
        if(root == null) {
            return null;
        }else {
            return root.searchTargetNode(value);
        }
    }

    //找到待删除结点的父节点
    public Node searchParentNode(int value) {
        if(root == null) {
            return null;
        }else {
            return root.searchParentNode(value);
        }
    }

    public int findRightTreeMin(Node node) {
        Node cur = node;
        while(cur.left != null) {
            cur = cur.left;
        }
        //此时就找到了待删除结点右边的最小值
        delNode(cur.value); //将最小值结点删除
        return cur.value;
    }
    //删除结点
    public void delNode(int value) {
        if(root == null) {
            return; //没有根结点,直接结束
        }else {
            //1.首先寻找待删除的结点
            Node targetNode = root.searchTargetNode(value);
            //若没有找到此结点
            if(targetNode == null) {
                System.out.println("没有此结点!");
                return;
            }
            //如果发现此二叉排序树只有一个根节点
            if(root.left == null && root.right == null) {
                root = null;
                return;
            }
            //2.寻找该结点的父节点
            Node parentNode = root.searchParentNode(value);
            /* ***第1种情况:待删除结点是叶子节点*** */
            if(targetNode.left == null && targetNode.right == null) { //叶子节点的判断
                //在这里判断左或右子结点为null,为了防止父节点只有一个子节点
                if(parentNode.left != null && parentNode.left.value == value) {
                    parentNode.left = null;
                }else if(parentNode.right != null && parentNode.right.value == value) {
                    parentNode.right = null;
                }
            }else  if(targetNode.left != null && targetNode.right != null) {
                /* ***第2种情况:待删除结点有两棵子树*** */
                //两种方案:1.往左边找到最大的替换。2.往右边找到最小的替换
                //在这里我采用右边找最大
                int min = findRightTreeMin(targetNode.right);
                targetNode.value = min;
            }else {
                /* ***第3种情况:待删除结点只有一棵子树*** */
                //在这里可能会遇到一种情况:只剩下一个根节点和一个子树
                //若待删除结点是这个根结点,则此时它没有父节点,需要单独考虑
                if(parentNode == null) {
                    if(targetNode.left != null) {
                        root = targetNode.left;
                    }else {
                        root = targetNode.right;
                    }
                }else {
                    /*有父节点的情况在这里细分:待删除结点是父节点的左或者右结点 + 待删除结点的左或者右结点存在
                    综上所述,一共四种情况,分开考虑*/
                    //待删除结点的左子节点存在
                    if(targetNode.left != null) {
                        //待删除结点是父节点的左子结点
                        if(parentNode.left.value == value) {
                            parentNode.left = targetNode.left;
                        }else { //待删除结点是父节点的右子结点
                            parentNode.right = targetNode.left;
                        }
                    }
                    //待删除结点的右子节点存在
                    if(targetNode.right != null) {
                        if(parentNode.left.value == value) {
                            parentNode.left = targetNode.right;
                        }else {
                            parentNode.right = targetNode.right;
                        }
                    }
                }
            }
        }

    }
}
class Node {
    public int value;
    public Node left;
    public Node right;

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

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

    /**
     * 查找待删除的结点
     * @param value 待删除结点的value值
     * @return  待删除的结点
     */
    public Node searchTargetNode(int value) {
        if(this.value == value) { //找到该结点
            return this;
        }else if(value < this.value) { //向左边找
            if(this.left == null) {
                return null; //左子结点没有值。直接返回null
            }
            return this.left.searchTargetNode(value); //向左递归查找
        }else {
            if(this.right == null) {
                return null;
            }
            return this.right.searchTargetNode(value);
        }
    }

    //不能自己删除自己,要找到父节点辅助删除!
    /**
     * 查找待删除结点的父节点
     * @param value 待删除结点的value值
     * @return  待删除结点的父结点
     */
    public Node searchParentNode(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.searchParentNode(value);
        }else if(value > this.value && this.right != null){
            return this.right.searchParentNode(value);
        }else {
            return null;
        }
    }
    //创建二叉树中添加结点
    public void add(Node node) {
        if(node == null) {
            return;
        }
        //判断传入的结点与当前子树的根节点的关系
        if(node.value < this.value) {
            if(this.left == null) {
                this.left = node;
            }else {
                this.left.add(node);
            }
        }else {
            if(this.right == null) {
                this.right = node;
            }else {
                this.right.add(node);
            }
        }
     }

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

平衡二叉树

看一个案例

给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.

在这里插入图片描述

存在的问题分析:

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

基本介绍

  1. 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树,可以说是二叉搜索树的升级版, 可以保证查询效率较高。

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

  3. avl树举例:

    平衡:在这里插入图片描述

    不平衡:在这里插入图片描述

平衡二叉树的旋转

左旋

给你一个数列,创建出对应的平衡二叉树.数列{4,3,6,5,7,8}

右子树的高度比左子树"高",需要左旋

思路过程:

在这里插入图片描述

代码实现:

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

}
右旋

给你一个数列,创建出对应的平衡二叉树.数列{10,12,8,9,7,6}

左子树的高度比右子树"高",需要右旋

思路过程:

在这里插入图片描述

代码实现:

 public void rightRotate() {
        Node newNode = new Node(this.value);
        newNode.right = this.right;
        newNode.left = this.left.right;
        this.value = this.left.value;
        this.left = this.left.left;
        this.right = newNode;
}
双旋转

前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列int[]arr={10,11,7,6,8,9};

运行原来的代码可以看到,并没有转成AVL树.

int[]arr={2,1,6,5,7,3};//运行原来的代码可以看到,并没有转成AVL树

思路:

在这里插入图片描述

  • 符合右旋转条件时,如果它的左孩子的右子树高度大于它的左孩子左子树的高度,先对当前这个结点的左结点进行左旋转,在对当前结点进行右旋转即可。
  • 符合左旋转条件时,如果它的右孩子树的左子树的高度大于它的右孩子的右子树的高度,先对右子结点进行右旋转,再对当前结点进行左旋转即可。
avl树代码汇总
package cn.ysk.avltree;

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 };
        AvlTree avlTree = new AvlTree();
        //添加结点
        for(int i=0; i < arr.length; i++) {
            avlTree.add(new Node(arr[i]));
        }
        avlTree.infixOrder();
        System.out.println("树的高度=" + avlTree.getRoot().getHeight()); //3
        System.out.println("树的左子树高度=" + avlTree.getRoot().getLeftTreeHeight()); // 2
        System.out.println("树的右子树高度=" + avlTree.getRoot().getRightTreeHeight()); // 2
        System.out.println(avlTree.getRoot());
    }

}
class AvlTree {
    private Node root;

    public Node getRoot() {
        return root;
    }

    public void add(Node node) {
        if(root == null) {
            root = node;
        }else {
            root.add(node);
        }
    }

    //中序遍历
    public void infixOrder() {
        if(root != null) {
            root.infixOrder();
        }else {
            System.out.println("空树,无法进行中序遍历!");
        }
    }
}
class Node {
    public int value;
    public Node left;
    public Node right;
    AvlTree avlTree = new AvlTree();
    public Node(int value) {
        this.value = value;
    }

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

    //创建二叉树中添加结点
    public void add(Node node) {
        if(node == null) {
            return;
        }
        //判断传入的结点与当前子树的根节点的关系
        if(node.value < this.value) {
            if(this.left == null) {
                this.left = node;
            }else {
                this.left.add(node);
            }
        }else {
            if(this.right == null) {
                this.right = node;
            }else {
                this.right.add(node);
            }
        }

        // ** 在这里共四种情况:**
        //当添加完一个结点后,如果: (右子树的高度-左子树的高度) > 1 , 左旋转
        int l = getLeftTreeHeight();
        int r = getRightTreeHeight();

        if(r-l > 1) {  //右边比左边高,左旋
            //如果它的右孩子树的左子树的高度大于它的右孩子的右子树的高度
            if(right != null && right.getLeftTreeHeight() > right.getRightTreeHeight()) {
                //先对右子结点进行右旋转
                right.rightRotate();
                //然后在对当前结点进行左旋转
                leftRotate(); //左旋转..
            } else {
                //直接进行左旋转即可
                leftRotate();
            }
            return ; //必须要!!!
        }
        //当添加完一个结点后,如果 (左子树的高度 - 右子树的高度) > 1, 右旋转
        if(l-r > 1) {
            //如果它的左孩子的右子树高度大于它的左孩子左子树的高度
            if(this.left != null && this.left.getRightTreeHeight() > this.left.getLeftTreeHeight()) {
                //先对当前结点的左结点(左子树)->左旋转
                this.left.leftRotate();
                //再对当前结点右旋
                rightRotate();
            }else {
                //直接右旋即可
                rightRotate();
            }
        }

    }

    //中序遍历
    public void infixOrder() {
        if(this.left != null) {
            this.left.infixOrder();
        }
        System.out.println(this);
        if(this.right != null) {
            this.right.infixOrder();
        }
    }
    //返回的左子树的高度
    public int getLeftTreeHeight() {
        if(this.left == null) {
            return 0;
        }
        return this.left.getHeight();
    }
    //返回右子树的高度
    public int getRightTreeHeight() {
        if(this.right == null) {
            return 0;
        }
        return this.right.getHeight();
    }

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

    }

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

    }

    public void rightRotate() {
        Node newNode = new Node(this.value);
        newNode.right = this.right;
        newNode.left = this.left.right;
        this.value = this.left.value;
        this.left = this.left.left;
        this.right = newNode;
    }
}

第11章 多路查找树

二叉树与B树

二叉树的问题分析

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

在这里插入图片描述

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

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

多叉树

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

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

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

在这里插入图片描述

B树的基本介绍

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

在这里插入图片描述

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

2-3树

2-3树基本介绍

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

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

2-3树应用案例

将数列{16,24,12,32,14,26,34,10,8,28,38,20}构建成2-3树,并保证数据插入的大小顺序。(演示一下构建2-3树的过程.)

在这里插入图片描述

插入规则:

  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树。

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

在这里插入图片描述

对B树的说明:

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

B+树的介绍

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

在这里插入图片描述

对B+树的说明:

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

B*树的介绍

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

在这里插入图片描述

对B*树的说明:

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

第12章 图

图的基本介绍

为什么要有图?

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

图的举例说明

图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。 结点也可以称为顶点。如图:

在这里插入图片描述

图的常用概念

  • 顶点(vertex)

  • 边(edge)

  • 路径

  • 无向图

在这里插入图片描述

  • 有向图

  • 带权图

在这里插入图片描述

图的表示方式

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

邻接矩阵

邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是的row和col表示的是1…n个点。

在这里插入图片描述

邻接表

  • 邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失.
  • 邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成

在这里插入图片描述

图的快速入门案例(创建)

代码实现下列结构:

在这里插入图片描述

思路:(1)存储顶点String使用ArrayList (2)保存矩阵int [][] edges

代码实现:

核心代码:

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

    //添加边的信息,两个顶点之间是否相连
    /**
     * @param v1 顶点1
     * @param v2  顶点2
     * @param value 两个顶点是否相连,相连是1,否则,是2!
     */
    public void insertEdge(int v1,int v2,int value) {
        edges[v1][v2] = value; //注意为无向图!
        edges[v2][v1] = value;
        numOfEdges++;  //边的数目加1
    }

图的深度优先遍历

图遍历介绍

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

深度优先遍历基本思想

图的深度优先搜索(DepthFirstSearch)。类似于二叉树的前序遍历!

无的图的深度优先搜索过程是这样的:首先访问指定的起始顶点v,然后选取与v邻接的未被访问过的任一顶点w访问,再选取与w邻接的未访问过的任一顶点访问,以此类推,重复上述过程。当到达一个所有邻接点都已经被访问过的顶点时,则按刚才的访问顺序向前回溯一个顶点。再重复上述过程,直到已访问过的顶点,不在有未被访问的邻接点为止。若图中还有未被访问的顶点,则任选一个未被访问的顶点访问,然后重复上述搜索过程,直到图中全部顶点都被访问过为止。这里的搜索次序体现了优先向深度发展的趋势,故称为深度优先搜索。

细节:以上面的邻接矩阵为例:当访问第一个结点(A)时,找到与它相邻的邻接点(比如上图A的第一个邻接点是B),只要找到,就去访问B的邻接点。

代码实现

//深度优先遍历算法,i的起始值是0
    public void dfs(int i) {
        System.out.print(getValueByIndex(i)+"->");
        visited[i] = true;
        for (int j = 0; j < vertexList.size(); j++) {
            //依次检查邻接矩阵v所在的行
            if((edges[i][j] != 0) && (!visited[j])) {
                dfs(j);
            }
        }
    }

    //对dfs进行一个重载,遍历我们所有的结点,并进行dfs
    public void dfs() {
        for (int i = 0; i < vertexList.size(); i++) {
            if(!visited[i]) {
                dfs(i);
            }
        }
    }

图的广度优先遍历

广度优先遍历基本思想

  1. 图的广度优先搜索(BroadFirstSearch)。
  2. 类似于树的按层次遍历,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
  3. 广度优先搜索是从图的某个节点v出发,在访问v之后吗,依次搜索的各个未被访问的邻接点w1,w2,w3······,wn。然后顺序搜索访问w1的各被访问过的邻接点。w2各未被访问过的邻接点,即从v开始,由近至远,按层次依次访问与v有路径相通且路径长度分别为1,2,···,n的顶点,直至连通图中所有顶点都被访问一次。这时,这时若图中还有未被访问的顶点,则任选一个未被访问的顶点访问。然后重复上述搜索过程,直到图中全部顶点都被访问过为止。

细节:深度优先是找到一个邻接点a之后遍历a结点的邻接点,广度优先是找到矩阵它所有的邻接点啊a,b,c,d···,再依次访问a,b,c,d···符合要求的结点

代码实现

 //广度优先遍历
    //先输出,输出完了之后再入队,入队是为了找它的邻接点
    public void bfs(int v) {
        System.out.print(getValueByIndex(v)+"->"); //访问第v个结点
        visited[v] = true;
        LinkedList<Integer> queue = new LinkedList();//这里用它的addLast和removeFirst方法来模拟队列
        queue.addLast(v); //将结点添加到队列
        while (!queue.isEmpty()) { //队列不为空
            Integer i = queue.removeFirst();//出队
            for (int j = 0; j < vertexList.size(); j++) { //依次搜索vi的邻接点
                if((edges[i][j] != 0) && !visited[j] ) { //若vi未被访问
                    System.out.print(getValueByIndex(j) + "->");
                    visited[j] = true;
                    queue.addLast(j);
                }
            }
        }
    }

    public void bfs() {
        for (int i = 0; i < vertexList.size(); i++) {
            if(!visited[i]) {
                bfs(i);
            }
        }
    }

图的代码汇总

package cn.ysk.graph;

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

public class Graph {
    private ArrayList<String> vertexList; //存储点的集合
    private int[][] edges; //对应的邻接矩阵
    private int numOfEdges; //边的数目
    private boolean[] visited;
    public static void main(String[] args) {
        String vertexs[] = {"A", "B", "C", "D", "E"};
        int n = 5;
        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); // A-B
		graph.insertEdge(0, 2, 1); //
		graph.insertEdge(1, 2, 1); //
		graph.insertEdge(1, 3, 1); //
		graph.insertEdge(1, 4, 1); //
        graph.showGraph();

        System.out.println("深度优先遍历:");
        graph.dfs();

//        System.out.println("广度优先遍历:");
//        graph.bfs();
    }

    //构造器
    public Graph(int n) {
        edges = new int[n][n];
        vertexList = new ArrayList<>();
        numOfEdges = 0;  //这里可不初始化,默认值就是0
        visited = new boolean[n];
    }

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

    //添加边的信息,两个顶点之间是否相连
    /**
     * @param v1 顶点1
     * @param v2  顶点2
     * @param value 两个顶点是否相连,相连是1,否则,是2!
     */
    public void insertEdge(int v1,int v2,int value) {
        edges[v1][v2] = value; //注意为无向图!
        edges[v2][v1] = value;
        numOfEdges++;  //边的数目加1
    }



    //深度优先遍历算法,i的起始值是0
    public void dfs(int i) {
        System.out.print(getValueByIndex(i)+"->");
        visited[i] = true;
        for (int j = 0; j < vertexList.size(); j++) {
            //依次检查邻接矩阵v所在的行
            if((edges[i][j] != 0) && (!visited[j])) {
                dfs(j);
            }
        }
    }

    //对dfs进行一个重载,遍历我们所有的结点,并进行dfs
    public void dfs() {
        for (int i = 0; i < vertexList.size(); i++) {
            if(!visited[i]) {
                dfs(i);
            }
        }
    }

    //广度优先遍历
    //先输出,输出完了之后再入队,入队是为了找它的邻接点
    public void bfs(int v) {
        System.out.print(getValueByIndex(v)+"->"); //访问第v个结点
        visited[v] = true;
        LinkedList<Integer> queue = new LinkedList();//这里用它的addLast和removeFirst方法来模拟队列
        queue.addLast(v); //将结点添加到队列
        while (!queue.isEmpty()) { //队列不为空
            Integer i = queue.removeFirst();//出队
            for (int j = 0; j < vertexList.size(); j++) { //依次搜索vi的邻接点
                if((edges[i][j] != 0) && !visited[j] ) { //若vi未被访问
                    System.out.print(getValueByIndex(j) + "->");
                    visited[j] = true;
                    queue.addLast(j);
                }
            }
        }
    }

    public void bfs() {
        for (int i = 0; i < vertexList.size(); i++) {
            if(!visited[i]) {
                bfs(i);
            }
        }
    }

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

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

    //返回边的数目
    public int getNumOfEdges() {
        return numOfEdges;
    }

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

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

程序员常用的十大算法

二分查找算法

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

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

代码实现

package cn.ysk.binarysearchnorecursion;

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

    /**
     * 二分查找的非递归实现
     * @param arr 待查找的数组,升序
     * @param target    待查找的值
     * @return  target对应的下标
     */
    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;
    }
}

分治算法

分治算法的介绍

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

分治算法的基本步骤

分治法在每一层递归上都有三个步骤:

  1. 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
  2. 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
  3. 合并:将各个子问题的解合并为原问题的解。

汉诺塔问题

引入:

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

思路分析
  • 如果是有一个盘, A->C

  • 如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的盘 2.

    1. 先把 最上面的盘 A->B

    2. 把最下边的盘 A->C

    3. 把B塔的所有盘 从 B->C

代码实现
package cn.ysk.hanoitower;

public class Hanoitower {
    public static void main(String[] args) {
        hanoiTower(4, 'A', 'B', 'C');
    }

    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.最下边的一个盘 2.上面的所有盘
            //1.先把最上面的所有盘A->B,移动过程会使用到c
            hanoiTower(num-1, a, c, b);
            //2.把最下边的盘A->C
            System.out.println("第"+num+"个盘从"+a+"->"+c);
            //3.把B塔的所有盘从B->C,移动过程使用到a塔
            hanoiTower(num-1, b, a, c);
        }
    }
}

动态规划算法

动态规划算法介绍

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

应用场景-背包问题

背包问题:有一个背包,容量为4磅 , 现有如下物品

物品重量价格
吉他(G)11500
音响(S)43000
电脑(L)32000
  1. 要求达到的目标为装入的背包的总价值最大,并且重量不超出

  2. 要求装入的物品不能重复(01背包问题)

思路分析和图解:
  • 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)

  • 这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。

  • 算法的主要思想,利用动态规划来解决。每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值。则我们有下面的结果:

    1. v[i][0]=v[0][j]=0; //表示 填入表 第一行和第一列是0

    2. 包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);

    3. 还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}。

      其中V(i-1,j)表示不装,V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i),但价值增加了v(i);

      为什么能装的情况下,需要这样求解?

      可以这么理解,如果要到达V(i,j)这一个状态有几种方式?

      **肯定是两种,第一种是第i件商品没有装进去,第二种是第i件商品装进去了。**没有装进去很好理解,就是V(i-1,j);装进去了怎么理解呢?如果装进去第i件商品,那么装入之前是什么状态,肯定是V(i-1,j-w(i))。由于最优性原理(上文讲到),V(i-1,j-w(i))就是前面决策造成的一种状态,后面的决策就要构成最优策略。两种情况进行比较,得出最优。

在这里插入图片描述

讲解比较好的博文:

代码:

代码实现

package cn.ysk.DynamicProgramming;

import java.util.Arrays;

//动态规划法解决01(不允许重复)背包问题
public class KnapsackProblem {
    public static void main(String[] args) {
        int[] w = {1,4,3}; //重量
        int[] val = {1500,3000,2000}; //价值
        int m = 4;//背包的重量
        int n = val.length; //物品的个数
//        声明动态规划表.其中v[i][j]对应于:当前有i个物品可选,并且当前背包的容量为j时,我们能得到的最大价值
        int[][] v = new int[n+1][m+1];
        //定义一个数组记录商品的保存情况
        int[][] path = new int[n+1][m+1];       

        for (int i = 1; i < v.length; i++) {
            for (int j = 1; j < v[0].length; j++) {
                if(j < w[i-1]) {
                    // 包的容量比当前该物品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
                    v[i][j] = v[i-1][j];
                }else { //这里分两种情况,装了第i件商品或者未装~
                    //由于最优性原理,V(i-1,j-w(i))就是前面决策造成的一种状态
                    if(val[i-1] + v[i-1][j-w[i-1]] > v[i-1][j]) {
                        v[i][j] = val[i-1] + v[i-1][j-w[i-1]];
                        path[i][j] = 1;
                    }else {
                        v[i][j] = v[i-1][j];
                    }
                }
            }
        }

        System.out.println();
        System.out.println("动态规划表如下:");
        for (int i = 0; i < n + 1; i++) {
            for (int j = 0; j < m + 1; j++) {
                System.out.print(v[i][j] + "\t");
            }
            System.out.println();
        }

       int i = path.length-1; //行的最大下标
       int j = path[0].length-1; //列的最大下标
       while (i>0 && j>0){
           if(path[i][j] == 1) {
               System.out.println("第"+i+"号商品放入背包!");
               j-=w[i-1];
           }
           i--;
       }
    }
}

KMP算法

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

字符串匹配问题:

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

暴力匹配算法

如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:

  1. 如果当前字符匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个字符

  2. 如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

  3. 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。(不可行!)

  4. 代码实现:

    package cn.ysk.kmp;
    
    public class ViolenceMatch {
        public static void main(String[] args) {
            String str1="硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好";
            String str2="尚硅谷你尚硅你";
            int index=violenceMatch(str1,str2);
            System.out.println("index="+index);
        }
        
        public static int violenceMatch(String str1,String str2) {
            char[] s1 = str1.toCharArray();
            char[] s2 = str2.toCharArray();
            int s1Length = s1.length;
            int s2Length = s2.length;
            int i = 0;
            int j = 0;
            while(i<s1Length && j<s2Length ) {
                if(s1[i] == s2[j]) {
                    i++;
                    j++;
                }else {//如果失配(即str1[i]!=str2[j]),令i=i-(j-1),j=0。
                    i = i-j+1;
                    j = 0;
                }
            }
            //判断是否匹配成功
            if(j == s2Length) {
                return i-j;
            }else {
                return -1;
            }
        }
    }
    

    KMP算法介绍

    1. KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法

    2. Knuth-Morris-Pratt字符串查找算法,简称为“KMP算法”,常用于在一个文本串S内查找一个模式串P的出现位置,这个算法由DonaldKnuth、VaughanPratt、JamesH.Morris三人于1977年联合发表,故取这3人的姓氏命名此算法.

    3. KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间

      kmp算法好的博客:

    代码实现

    package cn.ysk.kmp;
    
    import java.util.Arrays;
    
    public class KMPAlgorithm {
        public static void main(String[] args) {
            String str1 = "BBC ABCDAB ABCDABCDABDE";
            String str2 = "ABCDABD";
            int[] next = kmpNext(str2);
            int index = kmpSearch(str1, str2, next);
            System.out.println(index);
        }
    
        /**
         *
         * @param str1 源字符串
         * @param str2 字串
         * @param next  匹配值表
         * @return  匹配成功位置的索引
         */
        public static int kmpSearch(String str1,String str2,int[] next) {
            for (int i = 0,j = 0; i < str1.length(); i++) {
                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;
        }
    
        //获取到一个字符串(子串) 的部分匹配值表
        public static int[] kmpNext(String dest) {
            //创建一个next 数组保存部分匹配值
            int[] next = new int[dest.length()];
            next[0] = 0; //字符串的长度为1,则匹配值是0
            for (int i = 1,j = 0; i < dest.length(); i++) {
                //当dest.charAt(i)!=dest.charAt(j),我们需要从next[j-1]获取新的j
                //往前回退
                while (j>0 && dest.charAt(i) != dest.charAt(j)) {
                    j = next[j-1];  //kmp算法的核心
                }
                //当dest.charAt(i)==dest.charAt(j)满足时,部分匹配值就是+1
                if(dest.charAt(i) == dest.charAt(j)) {
                    j++;
                }
                next[i] = j;
            }
            return next;
        }
    }
    
    

贪心算法

贪心算法介绍

  1. 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法
  2. 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果

贪心算法最佳应用-集合覆盖

假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号

广播台覆盖地区
K1“北京”, “上海”, “天津”
K2“广州”, “北京”, “深圳”
K3“成都”, “上海”, “杭州”
K4“上海”, “天津”
K5“杭州”, “大连”

思路分析: 如何找出覆盖所有地区的广播台的集合呢,使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。假设总的有n个广播台,则广播台的组合总共有2ⁿ -1 个,假设每秒可以计算10个子集, 如图:

广播台数量n子集总数2ⁿ需要的时间
5323.2秒
101024102.4秒
32429496729613.6年
1001.26*100³º4x10²³年

使用贪婪算法,效率高:
目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:

  1. 遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系)
  2. 将这个电台加入到一个集合中(比如ArrayList), 想办法把该电台覆盖的地区在下次比较时去掉。
  3. 重复第1步直到覆盖了全部的地区

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

代码实现:

package cn.ysk.greedy;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

public class GreedyAlgorithm {
    public static void main(String[] args) {
        //创建广播电台
        HashMap<String,HashSet<String>> broadcasts = new HashMap<>();

        HashSet<String> hashSet1 = new HashSet<>();
        hashSet1.add("北京");
        hashSet1.add("上海");
        hashSet1.add("天津");

        HashSet<String> hashSet2 = new HashSet<>();
        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);
        //所有的地区
        HashSet<String> allAreas = new HashSet<>();
        allAreas.add("北京");
        allAreas.add("上海");
        allAreas.add("天津");
        allAreas.add("广州");
        allAreas.add("深圳");
        allAreas.add("成都");
        allAreas.add("杭州");
        allAreas.add("大连");

        //最终选择的集合
        ArrayList<String> selects = new ArrayList<>();
        //定义一个临时的集合,在遍历的过程中,存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的交集
        String maxKey = null;
        HashSet<String> temSet = new HashSet<>();
        while (allAreas.size() != 0) {
            maxKey = null;
            for (String key : broadcasts.keySet()) {
                temSet.clear();
                //当前的key能覆盖的地区
                HashSet<String> areas = broadcasts.get(key);
                temSet.addAll(areas);
                temSet.retainAll(allAreas); //求出两个集合的交集,结果会赋值给temSet
                if(temSet.size()>0 && (maxKey ==null || temSet.size() > broadcasts.get(maxKey).size())) {
                    maxKey = key;
                }
            }
            if(maxKey != null) {
                selects.add(maxKey);
                将maxKey指向的广播电台覆盖的地区,从allAreas去掉
                allAreas.removeAll(broadcasts.get(maxKey));
            }
        }
        System.out.println(selects);
    }
}

普里姆算法

最小生成树

修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称MST。

  1. 给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树

  2. N个顶点,一定有N-1条边

  3. 包含全部顶点

  4. N-1条边都在图中

  5. 举例说明(如图:)
    求最小生成树的算法主要是普里姆算法克鲁斯卡尔算法

在这里插入图片描述

普里姆算法介绍

普利姆(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 cn.ysk.prim;

import java.util.Arrays;

public class PrimAlgorithm {
    public static void main(String[] args) {
        //测试看看图是否创建ok
        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 graph = new Mgraph(verxs);
        MinTree tree = new MinTree();
        tree.creatGraph(graph, verxs, data, weight);
        tree.showWeight(graph);
        tree.prim(graph, 1);
    }
}

class MinTree {
    /**
     *
     * @param graph 图对象
     * @param verxs 图的顶点个数
     * @param data  图的各顶点的值
     * @param weight   图的邻接矩阵
     */
    public void creatGraph(Mgraph graph,int verxs,char[] data,int[][] weight) {
        for (int i = 0; i < verxs; i++) {
            graph.data[i] = data[i];
            for (int j = 0; j < verxs; j++) {
                graph.weight[i][j] = weight[i][j];
            }
        }
    }

    //输出图的邻接矩阵
    public void showWeight(Mgraph graph) {
        for (int[] link: graph.weight) {
            System.out.println(Arrays.toString(link));
        }
    }

    /**
     *
     * @param graph 图
     * @param v 设置起始点
     */
    public void prim(Mgraph graph,int v) {
        //创建一个数组,标志此点是否被访问过
        int[] visited = new int[graph.verxs];
        visited[v] = 1;
        //找出的边由哪两个点相连
        int h1 = -1;
        int h2 = -1;
        int minWeight = 10000;//将minWeight初始成一个大数,后面在遍历过程中,会被替换
        for (int i = 1; i < graph.verxs; i++) { //顶点个数为n,则最终会创建出n-1条边
            for (int j = 0; j < graph.verxs; j++) { //j结点表示被访问过的结点
                for (int k = 0; k < graph.verxs; k++) { //k结点表示还没有访问过的结点
                    if(visited[j]==1 && visited[k]==0 && graph.weight[j][k] < minWeight) {
                        //替换minWeight(寻找已经访问过的结点和未访问过的结点间的权值最小的边)
                        minWeight = graph.weight[j][k];
                        h1 = j;
                        h2 = k;
                    }
                }
            }
            //找到一条最小的边
            System.out.println("边<"+graph.data[h1]+","+graph.data[h2]+">权值:"+minWeight);
            //将h2标记为已访问
            visited[h2] = 1;
            minWeight = 10000; //重置
        }
    }
}

class Mgraph {
    public int verxs;
    public char[] data;
    public int[][] weight;
    public Mgraph(int verxs) {
        this.verxs = verxs;
        data = new char[verxs];
        weight = new int[verxs][verxs];
    }
}

克鲁斯卡尔算法

克鲁斯卡尔算法介绍

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

克鲁斯卡尔算法图解说明

在含有n个顶点的连通图中选择n-1条边,构成一棵极小连通子图,并使该连通子图中n-1条边上权值之和达到最小,则称其为连通网的最小生成树。

在这里插入图片描述

例如,对于如上图G4所示的连通网可以有多棵权值总和不相同的生成树。

在这里插入图片描述

以上图G4为例,来对克鲁斯卡尔进行演示(假设,用数组R保存最小生成树结果)。

在这里插入图片描述

第1步:将边<E,F>加入R中。
边<E,F>的权值最小,因此将它加入到最小生成树结果R中。
第2步:将边<C,D>加入R中。
上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果R中。
第3步:将边<D,E>加入R中。
上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果R中。
第4步:将边<B,F>加入R中。
上一步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳过边<C,F>。将边<B,F>加入到最小生成树结果R中。
第5步:将边<E,G>加入R中。
上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果R中。
第6步:将边<A,B>加入R中。
上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路;因此,跳过边<F,G>。同理,跳过边<B,C>。将边<A,B>加入到最小生成树结果R中。

此时,最小生成树构造完成!它包括的边依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>

克鲁斯卡尔算法分析

根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问题:
问题一 对图的所有边按照权值大小进行排序。
问题二 将边添加到最小生成树中时,怎么样判断是否形成了回路。

问题一很好解决,采用排序算法进行排序即可。

问题二,处理方式是:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。然后每次需要将一条边添加到最小生成树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。

如何判断是否构成回路-举例说明(如图)

在这里插入图片描述

在将<E,F> <C,D> <D,E>加入到最小生成树R中之后,这几条边的顶点就都有了终点:

  • C的终点是F。
  • D的终点是F。
  • E的终点是F。
  • F的终点是F。
  1. 关于终点的说明:
    就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是"与它连通的最大顶点"。
  2. 因此,接下来,虽然<C,E>是权值最小的边。但是C和E的终点都是F,即它们的终点相同,因此,将<C,E>加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。【后面有代码说明】

代码实现

package cn.ysk.kruskal;

import java.util.Arrays;

public class KruskalCase {
    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[][] = {
                /*A*//*B*//*C*//*D*//*E*//*F*//*G*/
                /*A*/ {   0,  12, INF, INF, INF,  16,  14},
                /*B*/ {  12,   0,  10, INF, INF,   7, INF},
                /*C*/ { INF,  10,   0,   3,   5,   6, INF},
                /*D*/ { INF, INF,   3,   0,   4, INF, INF},
                /*E*/ { INF, INF,   5,   4,   0,   2,   8},
                /*F*/ {  16,   7,   6, INF,   2,   0,   9},
                /*G*/ {  14, INF, INF, INF,   8,   9,   0}};
        KruskalCase kruskalCase = new KruskalCase(vertexs,matrix);
//        kruskalCase.print();
//        EData[] edges = kruskalCase.getEdges();
//        System.out.println("排序前:"+Arrays.toString(edges));
//        kruskalCase.sort(edges);
//        System.out.println("排序后:" + Arrays.toString(edges));
        kruskalCase.kruskal();
    }

    public KruskalCase(char[] vertexs,int [][] matrix) {
        int vLen = vertexs.length;
        //初始化顶点
        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] != Integer.MAX_VALUE) {
                    edgeNum++;
                }
            }
        }
    }

    public void kruskal() {
        int index = 0;
        EData[] rets = new EData[edgeNum]; //结果数组
        int[] ends = new int[edgeNum]; 用于保存"已有最小生成树" 中的每个顶点在最小生成树中的终点
        EData[] edges = getEdges();  //获取图中所有边的集合
        System.out.println("边的集合:"+ Arrays.toString(edges));
        sort(edges);  //排序
        for (int i = 0; i < edgeNum; i++) {
            //获取到第i条边的第一个顶点(起点)
            int p1 = getPosition(edges[i].start); //p1=4
            //获取到第i条边的第2个顶点
            int p2 = getPosition(edges[i].end); //p2 = 5

            //获取p1这个顶点在已有最小生成树中的终点
            int m = getEnd(ends, p1); //m = 4
            //获取p2这个顶点在已有最小生成树中的终点
            int n = getEnd(ends, p2); // n = 5
            //是否构成回路
            if(m != n) { //没有构成回路
                ends[m] = n; // 设置m 在"已有最小生成树"中的终点 <E,F> [0,0,0,0,5,0,0,0,0,0,0,0]
                rets[index++] = edges[i]; //有一条边加入到rets数组
            }
        }
        //<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。
        //统计并打印 "最小生成树", 输出  rets
        System.out.println("最小生成树为");
        for(int i = 0; i < index; i++) {
            System.out.println(rets[i]);
        }
    }

    public void print() {
        System.out.println("邻接矩阵为:");
        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 ch 顶点的值,比如'A','B'
     * @return 返回ch顶点对应的下标,如果找不到,返回-1
     */
    private int getPosition(char ch) {
        for(int i = 0; i < vertexs.length; i++) {
            if(vertexs[i] == ch) {//找到
                return i;
            }
        }
        //找不到,返回-1
        return -1;
    }

    /**
     * 功能: 获取图中边,放到EData[] 数组中,后面我们需要遍历该数组
     * 是通过matrix 邻接矩阵来获取
     * EData[] 形式 [['A','B', 12], ['B','F',7], .....]
     * @return
     */
    public 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;
    }

    /**
     * 功能:对边进行排序处理, 冒泡排序
     * @param edges 边的集合
     */
    public void sort(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 temp = edges[j];
                    edges[j] = edges[j+1];
                    edges[j+1] = temp;
                }
            }
        }
    }

    /**
     * 功能: 获取下标为i的顶点的终点(), 用于后面判断两个顶点的终点是否相同
     * @param ends : 数组就是记录了各个顶点对应的终点是哪个,ends 数组是在遍历过程中,逐步形成
     * @param i : 表示传入的顶点对应的下标
     * @return 返回的就是 下标为i的这个顶点对应的终点的下标,
     */
    private int getEnd(int[] ends, int i) { // i = 4 [0,0,0,0,5,0,0,0,0,0,0,0]
        while(ends[i] != 0) {  //这里用while循环时是为了找出一个顶点的“最终”终点,例如1的终点是2,2的终点是3,则1的终点是3!
            i = ends[i];
        }
        return i;
    }
}

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

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

    @Override
    public String toString() {
        return "EData [<" + start + ", " + end + ">= " + weight + "]";
    }
}

迪杰斯特拉算法

迪杰斯特拉(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. 重复执行两步骤,直到最短路径顶点为目标顶点即可结束

在这里插入图片描述

代码实现

package cn.ysk.dijkstra;

import java.util.Arrays;

public class DijkstraAlgorithm {
    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对象
        Graph graph = new Graph(vertex, matrix);
        //测试, 看看图的邻接矩阵是否ok
        graph.showGraph();
        //测试迪杰斯特拉算法
        graph.dsj(6);//C
        graph.showDijkstra();
    }

}

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

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

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

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

    //迪杰斯特拉算法实现
    /**
     *
     * @param index 表示出发顶点对应的下标
     */
    public void dsj(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顶点的距离
            }
        }
    }
}

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

    //构造器
    /**
     *
     * @param length :表示顶点的个数
     * @param index: 出发顶点对应的下标, 比如G顶点,下标就是6
     */
    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

    }
    /**
     * 功能: 判断index顶点是否被访问过
     * @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点作为新的访问顶点(注意不是出发顶点)
     * @return
     */
    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 ) {
                min = dis[i];
                index = i;
            }
        }
        //更新 index 顶点被访问过
        already_arr[index] = 1;
        return index;
    }

    //显示最后的结果
    //即将三个数组的情况输出
    public void show() {

        System.out.println("==========================");
        //输出already_arr
        for(int i : already_arr) {
            System.out.print(i + " ");
        }
        System.out.println();
        //输出pre_visited
        for(int i : pre_visited) {
            System.out.print(i + " ");
        }
        System.out.println();
        //输出dis
        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)算法介绍

  1. 和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名
  2. 弗洛伊德算法(Floyd)计算图中任意两顶点之间的最短路径,迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
  3. 弗洛伊德算法 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,是以同样的方式获得

在这里插入图片描述

第一轮循环中,以A(下标为:0)作为中间顶点,距离表和前驱关系更新为:

在这里插入图片描述

分析如下:

  1. 以A顶点作为中间顶点是,B->A->C的距离由N->9,同理C到B;C->A->G的距离由N->12,同理G到C

  2. 更换中间顶点,循环执行操作,直到所有顶点都作为中间顶点更新后,计算结束

代码实现

package cn.ysk.floyd;

import java.util.Arrays;

public class FloydAlgorithm {
    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 };
        Graph graph = new Graph(vertex.length,matrix,vertex);
        graph.floyd();
        graph.show();
    }
}
// 创建图
class Graph {
    private char[] vertex; // 存放顶点的数组
    private int[][] dis; // 保存,从各个顶点出发到其它顶点的距离,最后的结果,也是保留在该数组
    private int[][] pre;// 保存到达目标顶点的前驱顶点

    // 构造器
    /**
     *
     * @param length
     *            大小
     * @param matrix
     *            邻接矩阵
     * @param vertex
     *            顶点数组
     */
    public Graph(int length, int[][] matrix, char[] vertex) {
        this.vertex = vertex;
        this.dis = matrix;
        this.pre = new int[length][length];
        // 对pre数组初始化, 注意存放的是前驱顶点的下标
        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();
            System.out.println();

        }
    }

    public void floyd() {
        int len = 0;
        //对中间顶点遍历, k 就是中间顶点的下标 [A, B, C, D, E, F, G]
        for (int k = 0; k < dis.length; k++) {
            //从i顶点开始出发 [A, B, C, D, E, F, G]
            for (int i = 0; i < dis.length; i++) {
                //到达j顶点 // [A, B, C, D, E, F, G]
                for (int j = 0; j < dis.length; j++) {
                    len = dis[i][k] + dis[k][j];
                    if(len < dis[i][j]) {
                        dis[i][j] = len;
                        pre[i][j] = pre[k][j];
                    }
                }
            }
        }
    }
}

马踏棋盘算法

马踏棋盘算法介绍

  1. 马踏棋盘算法也被称为骑士周游问题

  2. 将马随机放在国际象棋的8×8棋盘Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格

在这里插入图片描述

马踏棋盘游戏代码实现

  1. 马踏棋盘问题(骑士周游问题)实际上是**图的深度优先搜索(DFS)**的应用。

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

在这里插入图片描述

  1. 分析第一种方式的问题,并使用贪心算法(greedyalgorithm)进行优化。解决马踏棋盘问题.

在这里插入图片描述

代码实现:

package cn.ysk.horse;

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

public class HorseChessboard {
    private static int X;  //列
    private static int Y;  //行
    private static boolean visited[]; //标记此点是否被访问过
    private static boolean finished;  //标记是否所有的位置都被访问
    public static void main(String[] args) {
        System.out.println("骑士周游算法,开始运行~~");
        //测试骑士周游算法是否正确
        X = 8;
        Y = 8;
        int row = 1; //马儿初始位置的行,从1开始编号
        int column = 1; //马儿初始位置的列,从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 end = System.currentTimeMillis();
        System.out.println("共耗时: " + (end - start) + " 毫秒");

        //输出棋盘的最后情况
        for(int[] rows : chessboard) {
            for(int step: rows) {
                System.out.print(step + "\t");
            }
            System.out.println();
        }
    }

    public static void traversalChessboard(int[][] chessboard,int row,int column,int step) {
        chessboard[row][column] = step;
        visited[X * row + column] = true;  //标记此点已被访问
        ArrayList<Point> ps = next(new Point(column,row));
        sort(ps);
        while (!ps.isEmpty()) {
            Point p = ps.remove(0); //取出下一个可以走的位置
            if(!visited[p.y * X + p.x]) {
                traversalChessboard(chessboard, p.y, p.x, step+1);
            }
        }
        //判断马儿是否完成了任务,使用   step 和应该走的步数比较 ,
        //如果没有达到数量,则表示没有完成任务,将整个棋盘置0
        //说明: step < X * Y  成立的情况有两种
        //1. 棋盘到目前位置,仍然没有走完
        //2. 棋盘处于一个回溯过程
        if(step < X*Y && !finished) {
            chessboard[row][column] = 0;
            visited[X * row + column] = false;
        }else {
            finished = true;
        }
    }

    /**
     * 功能: 根据当前位置(Point对象),计算马儿还能走哪些位置(Point),并放入到一个集合中(ArrayList), 最多有8个位置
     * @param curPoint
     * @return
     */
    public static ArrayList<Point> next(Point curPoint) {
        ArrayList<Point> ps = new ArrayList<>();
        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) {
                int count1 = next(o1).size();
                int count2 = next(o2).size();
                if(count1 < count2) {
                    return -1;
                }else if(count1 == count2) {
                    return 0;
                }else {
                    return 1;
                }
            }
        });
    }
}

卖油翁和老黄牛

我亦无他,惟首熟尔!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值