赫夫曼树和赫夫曼编码及赫夫曼编码应用之数据文件压缩与解压详解与代码实现

一、赫夫曼树

1.1 赫夫曼树基本介绍

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

1.2 赫夫曼树的重要概念

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

1.3 构建赫夫曼树的步骤

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

1.4 赫夫曼树代码实现

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};
        Node root = createHuffmanTree(arr);
        preOrder(root);
    }

    // 编写一个前序遍历的方法
    public static void preOrder(Node root) {
    
    
        if (root != null) {
    
    
            root.preOrder();
        } else {
    
    
            System.out.println("空树,不能遍历");
        }
    }

    // 创建赫夫曼树的方法
    public static Node createHuffmanTree(int[] arr) {
    
    
        // 第一步为了操作方便
        // 1、遍历arr数组
        // 2、将arr的每个元素构建成一个Node
        // 3、将Node放入到ArrayList中
        List<Node> nodes = new ArrayList<Node>();
        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);
        }
        // 返回赫夫曼树的root结点
        return nodes.get(0);
    }


}

// 创建节点类
class Node implements Comparable<Node> {
    
    
    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.5 运行结果

在这里插入图片描述

二、赫夫曼编码

2.1 赫夫曼编码基本介绍

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

2.2 通信领域中信息的处理方式

2.2.1 定长编码

  原字符串共40个字符(包括空格),因为我们最终通信使用的都是二进制来传递信息,所以转换为二进制总的长度是359(包括空格)
在这里插入图片描述

2.2.2 变长编码

  同样还是这个字符串,共40个字符(包括空格),根据下面的编码方式,编码就是10010110100……,我们可以明显的感觉到编码长度变小了。当然,字符的编码都不能是其它字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码,否则我们的编码就乱了,也就造成了我们常说的乱码
在这里插入图片描述

2.3 赫夫曼编码原理剖析

在这里插入图片描述

  1. 原字符串共40个字符(包括空格),根据赫夫曼树,给各个字符规定编码,向左的路径为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
  1. 按照上面的赫夫曼编码,我们的“i like like like java do you like a java”字符串对应的编码如下,注意这里我们使用的无损压缩,长度为133
    在这里插入图片描述

  2. 原来的长度是359,压缩了 62.9 % 62.9\% 62.9%
    359 − 133 359 = 62.9 % \frac{359-133}{359}=62.9\% 359359133=62.9%

  3. 此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀,不会造成匹配的多义性

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

2.4 赫夫曼编码应用之数据文件压缩与解压代码实现

package 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[] contentBytes = content.getBytes();
        byte[] huffmanCodesBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果是:" + Arrays.toString(huffmanCodesBytes));
        byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
        System.out.println("原来的字符串:" + new String(sourceBytes));

    }

    // 编写方法,完成对压缩文件的解压
    public static void unZipFile(String zipFile, String dstFile) {
    
    
        // 先定义文件输入流
        InputStream is = null;
        // 定义一个对象输入流
        ObjectInputStream ois = null;
        // 定义文件的输出流
        OutputStream os = null;
        try {
    
    
            // 创建文件输入流
            is = new FileInputStream(zipFile);
            // 创建一个和is关联的对象输入流
            ois = new ObjectInputStream(is);
            // 读取byte数组
            byte[] huffmanBytes = (byte[]) ois.readObject();
            // 读取赫夫曼编码表
            Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();
            // 解码
            byte[] bytes = decode(huffmanCodes, huffmanBytes);
            // 将bytes数组写入到目标文件
            os = new FileOutputStream(dstFile);
            // 写出数据到dstFile文件
            os.write(bytes);
        }catch (Exception e){
    
    
            System.out.println(e.getMessage());
        }finally {
    
    
            try {
    
    
                os.close();
                ois.close();
                is.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    // 编写方法,将文件进行压缩
    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) {
    
    
            System.out.println(e.getMessage());
        } finally {
    
    
            try {
    
    
                is.close();
                os.close();
                oos.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    // 编写一个方法,完成对压缩数据的解码
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
    
    
        // 先得到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));
        }
        // 把字符串按照指定的赫夫曼编码进行解码
        Map<String, Byte> map = new HashMap<String, Byte>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
    
    
            map.put(entry.getValue(), entry.getKey());
        }
        // 创建一个集合,存放byte
        List<Byte> list = new ArrayList<>();
        // i可以理解成就是索引,扫描stringBuilder
        for (int i = 0; i < stringBuilder.length(); ) {
    
    
            int count = 1; // 小的计数器
            boolean flag = true;
            Byte b = null;
            while (flag) {
    
    
                // 递增的取出key
                String key = stringBuilder.substring(i, i + count); // i不动,让count移动,直到匹配到一个字符
                b = map.get(key);
                if (b == null) {
    
     // 说明没有匹配到
                    count++;
                } else {
    
     // 匹配到
                    flag = false;
                }
            }
            list.add(b);
            i += count; // i直接移动到count
        }
        // 当for循环结束后,list中就存放了所有的字符
        // 把list中的数据放到byte[]并返回
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
    
    
            b[i] = list.get(i);
        }
        return b;
    }

    // 完成数据的解压
    // 1、将huffmanCodesBytes重新先转成赫夫曼编码对应的二进制的字符串
    // 2、将赫夫曼编码对应的二进制的字符串对照赫夫曼编码重新转成原字符串
    private static String byteToBitString(boolean flag, byte b) {
    
    
        // 使用变量保存b
        int temp = b; // 将b转成int
        // 如果是正数,还存在一个补高位的问题
        if (flag) {
    
    
            temp |= 256;
        }
        String str = Integer.toBinaryString(temp); // 返回temp对应的二进制的补码
        if (flag) {
    
    
            return str.substring(str.length() - 8);
        } else {
    
    
            return str;
        }
    }

    // 使用一个方法,将前面的方法封装起来,便于我们的调用
    private static byte[] huffmanZip(byte[] bytes) {
    
    
        List<Node> nodes = getNodes(bytes);
        // 根据node创建赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        // 生成对应的赫夫曼编码(根据赫夫曼树)
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        // 根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组
        byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
        return huffmanCodeBytes;
    }

    // 编写一个方法,将字符串对应的btye[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的btye[]数组
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
    
    
        // 1、利用huffmanCodes将bytes转成赫夫曼编码对应的字符串
        StringBuilder stringBuilder = new StringBuilder();
        // 遍历bytes数组
        for (byte b : bytes) {
    
    
            stringBuilder.append(huffmanCodes.get(b));
        }
        //System.out.println("测试stringBuilder:" + stringBuilder.toString());
        // 将上面对应的字符串转成byte[]
        // 统计返回的长度
        // 一句话:int len = (stringBuilder.length() + 7) / 8
        int len;
        if (stringBuilder.length() % 8 == 0) {
    
    
            len = stringBuilder.length() / 8;
        } else {
    
    
            len = stringBuilder.length() / 8 + 1;
        }
        // 创建存储压缩后的byte[]
        byte[] by = new byte[len];
        int index = 0; // 计数器,记录是第几个byte
        for (int i = 0; i < stringBuilder.length(); i += 8) {
    
     // 因为每八位对应一个byte,所以步长为8
            String strByte;
            if (i + 8 > stringBuilder.length()) {
    
     // 不够8位
                strByte = stringBuilder.substring(i);
            } else {
    
    
                strByte = stringBuilder.substring(i, i + 8);
            }
            // 將strByte转成byte,放入到by
            by[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        return by;
    }

    // 生成赫夫曼树对应的赫夫曼编码
    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    static StringBuilder stringBuilder = new StringBuilder();

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

    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
    
    
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        // 将传入的code加入到stringBuilder2
        stringBuilder2.append(code);
        if (code != null) {
    
     // 如果为空结点,不处理
            // 判断当前node是叶子结点还是非叶子结点
            if (node.data == null) {
    
     // 非叶子结点
                // 递归处理
                // 向左递归
                getCodes(node.left, "0", stringBuilder2);
                // 向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {
    
     // 说明是一个叶子结点
                // 说明找到了某个叶子结点的最后
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }
    }

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

    private static List<Node> getNodes(byte[] bytes) {
    
    
        // 创建一个ArrayLst
        ArrayList<Node> nodes = new ArrayList<Node>();
        // 遍历bytes,统计每一个byte出现的次数
        Map<Byte, Integer> 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集合
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
    
    
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    // 通过List,创建对应的赫夫曼树
    private 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中移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 将新的二叉树加入到nodes
            nodes.add(parent);
        }
        // nodes最后的结点就是赫夫曼树的根节点
        return nodes.get(0);
    }

}

// 创建Node,带数据和权值
class Node implements Comparable<Node> {
    
    
    Byte data; // 存放数据本身
    int weight; // 权值,表示数据出现次数
    Node left;
    Node right;

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

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

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

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

}

2.5 运行结果

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/IronmanJay/article/details/112736829