数据结构与算法(赫夫曼树,赫夫曼编码)

赫夫曼树

 

基本介绍:

(1)给定n个权值作为n给叶子节点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称哈夫曼树(HuffmanTree),还有的树翻译为霍夫曼树.

(2)赫夫曼树是带权路径长度最短的树,权值较大的节点离根较近.

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

(1)路径和路径长度,在一颗树中,从一个节点往下可以达到的孩子或孙子节点之间的通路,称为路径.通路中分支的数目称为路径长度,若规定根节点的层数为1,则从根节点到第L层的节点路径长度为L-1

(2)节点的权及带权路径长度:若将树中节点赋给一个有着某种含义的数值,则这个数值称为该节点的权.节点的带权路径长度为:从根节点到该节点之前的路径长度与该节点的权的乘积.

(3)树的带权路径长度:树的带权路径长度规定为所有叶子节点的带权路径长度之和,记为WPL(weighted path length),权值越大的节点离根节点越近的二叉树才是最优二叉树.

(4)WPL最小的介绍赫夫曼树

数据结构与算法(赫夫曼树,赫夫曼编码)_第1张图片

赫夫曼树创建思路图解

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

思路分析

构建赫夫曼树的步骤:

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

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

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

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

(5)图解

数据结构与算法(赫夫曼树,赫夫曼编码)_第2张图片

 代码实现

package 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 = createHuffmanTree(arr);
        //测试
        preOrder(root);

    }
    //编写一个前序遍历方法
    public static void preOrder(Node root){
        if (root != null){
            root.preOrder();
        }else {
            System.out.println("该赫夫曼是空树");
        }
    }

    //创建赫夫曼树的方法
    /**
     *
     * @param arr  需要创建成赫夫曼树的数组
     * @return 创建和后的赫夫曼树的root节点
     */
    public static Node createHuffmanTree(int[] arr) {
        //第一步为了方便操作
        //1.遍历arr数组
        //2.将arr的每个元素构成一个Node
        //3.将Node 放入到ArrayList中
        List nodes = new ArrayList<>();
        for (int value : arr) {
            nodes.add(new Node(value));
        }
        //我们处理的过程是循环的过程
        while (nodes.size() > 1) {

            //排序从小到大
            Collections.sort(nodes);

            //取出根节点权值最小的两颗 二 叉树
            //(1)取出权值最小的节点(二叉树)
            Node leftNode = nodes.get(0);
            //(2)取出权值第二小的节点(二叉树)
            Node rightNode = nodes.get(1);
            //(3)构建一颗新的二叉树
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;

            //(4)从ArrayList删除处理过的二叉树
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //(5)将parent加入到nodes
            nodes.add(parent);
            // Collections.sort(nodes);
        }
        //返回赫夫曼树的root节点
        return nodes.get(0);
    }
}

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

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

    }

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

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

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

赫夫曼编码

基本介绍

(1)赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法

(2)赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一

(3)赫夫曼编码广泛地用于数据文件压缩.其压缩率通常在20%~90%之间

(4)赫夫曼编码是可变字长编码(VLC)的一种.Huffman于1952年提出一种编码方法,称之为最佳编码

原理剖析

数据结构与算法(赫夫曼树,赫夫曼编码)_第3张图片

数据结构与算法(赫夫曼树,赫夫曼编码)_第4张图片

 通信领域中信息的处理方式3-赫夫曼编码

 传输的字符串
(1)illike like like java do you like a java
(2) d:1y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9|/各个字符对应的个数
(3)按照上面字符出现的次数构建一颗赫夫曼树,次数作为权值
构成赫夫曼树的步骚:

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

数据结构与算法(赫夫曼树,赫夫曼编码)_第5张图片

(4)根据赫夫曼树,给各个字符,规定编码(前缀编码),向左的路径为o向右的路径为1,编码如下:o: 1000 u: 10010 d: 100110 y: 100111 i: 101
a : 110k: 1110 e: 1111j: 0000v: 0001l: 001:01
(5)按照上面的赫夫曼编码,我们的"ilike like like java do you like a java”字符串对应的编码为(注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110通过赫夫曼编码处理长度为133
(6)长度为:133
说明:
原来长度是359,压缩了(359-133)/359=62.9%
此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性

 注意事项

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

数据结构与算法(赫夫曼树,赫夫曼编码)_第6张图片

数据压缩(创建赫夫曼树)

将给出的一段文本,比如 "ilike like like java do you like a java”,根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理形式
"10101001101111011010011011011101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
步骤1:根据赫夫曼编码压缩数据的原理,需要创建"i like like like java do youlike a java”对应的赫夫曼树.

创建赫夫曼数核心代码:

 //通过list创建赫夫曼树
    private static Node createHuffmanTree(List nodes){
        while(nodes.size() >1){
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二轮最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有date ,只有权值
            Node parent = new Node(null,leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理的两颗二叉树从nodes删除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树,加入到nodes
            nodes.add(parent);
        }
        //nodes 最后的节点,就是赫夫曼树的根节点
        return nodes.get(0);
    }

数据压缩(生成赫夫曼编码和赫夫曼编码后的数据)

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

核心代码实现:

 /**
     * 为了方便调用,我们重载getCodes
     * @param root 根节点
     * @return huffmanCodes
     */
    private static MapgetCods(Node root){
        if (root == null){
            return  null;
        }
        //处理root的左子树
        getCods(root.left,"0",stringBuilder);
        //处理root右子树
        getCods(root.right,"1",stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合中
     * @param node 传入节点
     * @param code 路径 :左子节点0,右子节点1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCods(Node node, String code ,StringBuilder stringBuilder) {

        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将cods 加入到stringBuffer2
        stringBuilder2.append(code);
        if (node != null) { //如果node ==null不处理
            //判断当前node 是叶子节点还是非叶子节点
            if (node.date == null) {//非叶子节点
                //递归处理
                //向左递归
                getCods(node.left, "0", stringBuilder2);
                //向右递归
                getCods(node.right, "1", stringBuilder2);
            } else { //说明是一个叶子节点
                //就表示找到了某个叶子节点的最后
                huffmanCodes.put(node.date, stringBuilder2.toString());
            }
        }
    }

数据解压(使用赫夫曼编码解压)

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

(1)前面我们得到了赫夫曼编码和对应的编码
byte[],即:[-88,-6念,-56,-65,-56,-65,-55,77,-57,6,-24,-14,-117,-4,-60,-90,28]
(2)现在要求使用赫夫曼编码,进行解码,又
重新得到原来的字符串"i like like like
java do you like a java"
思路:解码过程,就是编码的一个逆向操作。

解压核心代码:

/**
     * 将一个byte 转成一个二进制的字符串
     * @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 huffmanCodes 赫夫曼编码表map
     * @param huffmanBytes 赫夫曼编码得到字节数组
     * @return 就是原来字符串对应的数组
     *  如何将数据进行解压(解码)
     *         思路
     *         1.将huffmanCodeBytes[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
     *         重写先转成赫夫曼编码对应的二进制的字符串"10101000010111...."
     *         2.赫夫曼编码对应的二进制的字符串"1010100010111..=>对照赫夫曼编码=>i like like like java do you like a java"
     */
    private static byte[] decode(Map huffmanCodes, byte[] huffmanBytes){

        //1.先得到huffmanBytes 对应的二进制的字符串,
        StringBuilder stringBuilder = new StringBuilder();
        //将byte数组转成二进制的字符串
        for (int i = 0 ; i < huffmanBytes.length; i++){
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag,b));
        }
        //把字符串按照指定的赫夫曼编码进行解码
        //把赫夫曼编码表进行调换,因为要反向查询 97->100 100->1
        Map map = new HashMap<>();
        for (Map.Entry entry: huffmanCodes.entrySet()){
            map.put(entry.getValue(),entry.getKey());
        }
        //创建一个集合,存放byte
        List list = new ArrayList<>();
        //i 可以理解成就是索引,扫描stringBuilder
        for (int i = 0 ;i < stringBuilder.length();){
            int count = 1;//小的计数器
            boolean flag = true;
            Byte b = null;
            while (flag){
                //取出一个'1' '0' 递增的取出key 1
                String key = stringBuilder.substring(i,i+count);//i不动,让count移动,匹配到指定一个字符
                b = map.get(key);
                if (b == null){
                    //说明没有匹配到
                    count++;
                }else {
                    //匹配到
                    flag = false;
                }
            }
            list.add(b);
            i +=count;//i直接移动到count
        }
        //当for循环结束后,我们list中就存放了所有的字符"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;
    }

文件压缩

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

代码实现

 /**
     * 编写一个方法,将一个文件进行压缩
     * @param srcFile  你传入希望压缩的文件的全路径
     * @param dstFile 我们压缩后将文件放到那个目录
     */
    public static void zipFile(String srcFile,String dstFile){
        //创建输出流
        OutputStream os = null;
        ObjectOutputStream oos = null;
        //创建文件输入流
        FileInputStream is =  null;
        try {
            //创建一个和源文件大小一样的byte[]
            is = new FileInputStream(srcFile);
            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){
            System.out.println(e.getMessage());
        }finally {
            try {
                is.close();
                oos.close();
                os.close();
            }catch (Exception e){
                System.out.println(e.getMessage());
            }
        }
    }

文件解压(文件恢复)

具体要求:将前面压缩的文件,重新恢复成原来的文件

思路:读取压缩文件(数据和和赫夫曼编码表)->完成解压

代码实现:

 /**
     * 编写一个方法,完成对压缩文件进行解压
     * @param zipFile 准备解压的文件
     * @param dstFile  将文件解压到那个路径
     */
    public static void unZipFile(String zipFile ,String dstFile){
        //定义文件输入流
        InputStream is = null;
        //定义对象输入流
        ObjectInputStream ois = null;
        //定义文件输出流
        OutputStream os = null;
        try{
            //创建文件输入流
            is = new FileInputStream(zipFile);
            //创建一个和is关联的对象输入流
            ois = new ObjectInputStream(is);
            //读取byte数组 huffmanByte
            byte [] huffmanBytes = (byte[])ois.readObject();
            //读取赫夫曼编码表
            Map huffmanCodes = (Map)ois.readObject();

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

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

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

(1)如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化,比如视频,ppt等文件

(2)赫夫曼编码是按字节来处理的,因此可以处理所有文件(二进制,文本文件)

(3)如果一个文件中的内容,重复数据不多,压缩效果也不会很明显

代码汇总

前面所有的代码

package huffmanCode;

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

/**
 * 赫夫曼编码  压缩和解码
 */
public class HuffmanCode {

    public static void main(String[] args) {
        /*
        //测试压缩文件
        String srcFile = "D:\\新建 文本文档.txt";
        String dstFile ="D:\\dst2.zip" ;

        zipFile(srcFile,dstFile);
        System.out.println("压缩文件ok~~~");
         */

        //测试解压文件
        String zipFile ="D:\\dst2.zip" ;
        String dstFile = "D:\\111111111112.jpg";
        unZipFile(zipFile,dstFile);
        System.out.println("解压成功");

        /*
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        System.out.println(contentBytes.length); //40

        byte[] huffmanCodesBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果=" + Arrays.toString(huffmanCodesBytes) + "长度=" + huffmanCodesBytes.length);

        //测试byteToBitString方法
        //System.out.println(byteToBitString((byte)1));

        byte[] sourceBytes = decode(huffmanCodes,huffmanCodesBytes);
        System.out.println("原来的字符串="+ new String(sourceBytes));
         */
        //分步过程
        /*
       List nodes = getNodes(contentBytes);
        System.out.println(nodes);

        //测试一次,创建的二叉树
        System.out.println("赫夫曼树");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("前序遍历");
        huffmanTreeRoot.preOrder();

        //测试是否生成了对应的赫夫曼编码
        Map huffmanCodes = getCods(huffmanTreeRoot);
        System.out.println("~生成的赫夫曼编码表"+huffmanCodes);//17

        //测试
        byte[] huffmanCodeBytes = zip(contentBytes,huffmanCodes);
        System.out.println("huffmanCodeBytes="+Arrays.toString(huffmanCodeBytes));

        //发生huffmanCodeBytes 数组

       */
    }


    /**
     * 编写一个方法,将一个文件进行压缩
     * @param srcFile  你传入希望压缩的文件的全路径
     * @param dstFile 我们压缩后将文件放到那个目录
     */
    public static void zipFile(String srcFile,String dstFile){
        //创建输出流
        OutputStream os = null;
        ObjectOutputStream oos = null;
        //创建文件输入流
        FileInputStream is =  null;
        try {
            //创建一个和源文件大小一样的byte[]
            is = new FileInputStream(srcFile);
            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){
            System.out.println(e.getMessage());
        }finally {
            try {
                is.close();
                oos.close();
                os.close();
            }catch (Exception e){
                System.out.println(e.getMessage());
            }
        }
    }

    /**
     * 编写一个方法,完成对压缩文件进行解压
     * @param zipFile 准备解压的文件
     * @param dstFile  将文件解压到那个路径
     */
    public static void unZipFile(String zipFile ,String dstFile){
        //定义文件输入流
        InputStream is = null;
        //定义对象输入流
        ObjectInputStream ois = null;
        //定义文件输出流
        OutputStream os = null;
        try{
            //创建文件输入流
            is = new FileInputStream(zipFile);
            //创建一个和is关联的对象输入流
            ois = new ObjectInputStream(is);
            //读取byte数组 huffmanByte
            byte [] huffmanBytes = (byte[])ois.readObject();
            //读取赫夫曼编码表
            Map huffmanCodes = (Map)ois.readObject();

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

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

    /**
     *编写一个方法,完成对压缩数据的解码
     * @param huffmanCodes 赫夫曼编码表map
     * @param huffmanBytes 赫夫曼编码得到字节数组
     * @return 就是原来字符串对应的数组
     *  如何将数据进行解压(解码)
     *         思路
     *         1.将huffmanCodeBytes[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
     *         重写先转成赫夫曼编码对应的二进制的字符串"10101000010111...."
     *         2.赫夫曼编码对应的二进制的字符串"1010100010111..=>对照赫夫曼编码=>i like like like java do you like a java"
     */
    private static byte[] decode(Map huffmanCodes, byte[] huffmanBytes){

        //1.先得到huffmanBytes 对应的二进制的字符串,
        StringBuilder stringBuilder = new StringBuilder();
        //将byte数组转成二进制的字符串
        for (int i = 0 ; i < huffmanBytes.length; i++){
            byte b = huffmanBytes[i];
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag,b));
        }
        //把字符串按照指定的赫夫曼编码进行解码
        //把赫夫曼编码表进行调换,因为要反向查询 97->100 100->1
        Map map = new HashMap<>();
        for (Map.Entry entry: huffmanCodes.entrySet()){
            map.put(entry.getValue(),entry.getKey());
        }
        //创建一个集合,存放byte
        List list = new ArrayList<>();
        //i 可以理解成就是索引,扫描stringBuilder
        for (int i = 0 ;i < stringBuilder.length();){
            int count = 1;//小的计数器
            boolean flag = true;
            Byte b = null;
            while (flag){
                //取出一个'1' '0' 递增的取出key 1
                String key = stringBuilder.substring(i,i+count);//i不动,让count移动,匹配到指定一个字符
                b = map.get(key);
                if (b == null){
                    //说明没有匹配到
                    count++;
                }else {
                    //匹配到
                    flag = false;
                }
            }
            list.add(b);
            i +=count;//i直接移动到count
        }
        //当for循环结束后,我们list中就存放了所有的字符"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 转成一个二进制的字符串
     * @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 nodes = getNodes(bytes);
        //根据nodes创建赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //生成对应的赫夫曼编码
        Map huffmanCodes = getCods(huffmanTreeRoot);
        //根据生产的赫夫曼编码压缩,得到压缩后的赫夫曼编码字节数组
        byte[] huffmanCodeBytes = zip(bytes,huffmanCodes);
        return huffmanCodeBytes;
    }

    /**
     *编写一个方法,将字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的byte[]
     * @param bytes 中时原始的字符串对应的byte[]
     * @param huffmanCodes 生产的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的byte[]
     *
     */
    private static byte[] zip(byte[] bytes, Map huffmanCodes){

        //1.利用huffmanCodes 将bytes 转成赫夫曼编码的对应字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历bytes 数组
        for (byte b : bytes){
            stringBuilder.append(huffmanCodes.get(b));
        }
        //将 对应的字符串,转成byte[]
        //统计返回的byte[]huffmanCodeBytes 长度
        //一句话 int len =(stingBuilder.length()+7)/8;
        int len;
        if (stringBuilder.length() % 8 ==0){
            len =stringBuilder.length() / 8;
        }else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建存储压缩后的byte数组
        byte [] huffmanCodeBytes = new byte[len];
        int index = 0; //记录是第一个byte

        for (int i = 0; i < stringBuilder.length(); i += 8){
            //因为是每8位对应一个byte,所有步长是+8
            String strByte;
            if (i + 8 > stringBuilder.length()){ //不够八位
                strByte = stringBuilder.substring(i);
            }else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            //将strByte 转成一个byte,放入huffmanCodeBytes
            huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte,2);
            index++;
        }
        return huffmanCodeBytes;
    }

    /**
     * 生成赫夫曼树对应的编码表
     *     思路分析:
     *     1.将赫夫曼编码表存放在Map形式
     *     2.在生成赫夫曼编码表示,需要去拼接路径,定义一个StingBuilder 储存某个叶子节点的路径
     */
    static Map huffmanCodes = new HashMap<>();
    static StringBuilder stringBuilder =new StringBuilder();

    /**
     * 为了方便调用,我们重载getCodes
     * @param root 根节点
     * @return huffmanCodes
     */
    private static MapgetCods(Node root){
        if (root == null){
            return  null;
        }
        //处理root的左子树
        getCods(root.left,"0",stringBuilder);
        //处理root右子树
        getCods(root.right,"1",stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合中
     * @param node 传入节点
     * @param code 路径 :左子节点0,右子节点1
     * @param stringBuilder 用于拼接路径
     */
    private static void getCods(Node node, String code ,StringBuilder stringBuilder) {

        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //将cods 加入到stringBuffer2
        stringBuilder2.append(code);
        if (node != null) { //如果node ==null不处理
            //判断当前node 是叶子节点还是非叶子节点
            if (node.date == null) {//非叶子节点
                //递归处理
                //向左递归
                getCods(node.left, "0", stringBuilder2);
                //向右递归
                getCods(node.right, "1", stringBuilder2);
            } else { //说明是一个叶子节点
                //就表示找到了某个叶子节点的最后
                huffmanCodes.put(node.date, stringBuilder2.toString());
            }
        }
    }

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

    /**
     * 将字节数组存放到list中
     * @param bytes 接受字节数组
     * @return 返回的就是List 形式 [Node[date=97] ,weight = 5]
     */
    private static List getNodes(byte[] bytes) {
        //创建一个ArrayList
        ArrayList nodes = new ArrayList<>();
        //遍历bytes ,统计每一个byte出现的次数->map[key ,value]
        Map counts = new HashMap<>();
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) { //Map还没有这个字符数据,第一次
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }
        //把每个键值对转成一个Node 对象,并加入到nodes集合
        //遍历map
        for (Map.Entry entry : counts.entrySet()){
            nodes.add(new Node(entry.getKey(),entry.getValue()));
        }
        return nodes;
    }

    //通过list创建赫夫曼树
    private static Node createHuffmanTree(List nodes){
        while(nodes.size() >1){
            //排序,从小到大
            Collections.sort(nodes);
            //取出第一颗最小的二叉树
            Node leftNode = nodes.get(0);
            //取出第二轮最小的二叉树
            Node rightNode = nodes.get(1);
            //创建一颗新的二叉树,它的根节点没有date ,只有权值
            Node parent = new Node(null,leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //将已经处理的两颗二叉树从nodes删除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将新的二叉树,加入到nodes
            nodes.add(parent);
        }
        //nodes 最后的节点,就是赫夫曼树的根节点
        return nodes.get(0);
    }
}

//创建Node ,待数据和权值
class Node implements Comparable{
    Byte date; //存放数据
    int weight;//权值,表示字符出现的次数
    Node left;
    Node right;

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

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

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

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


 

你可能感兴趣的:(数据结构与算法,算法,数据结构)