[063] Java “文件压缩及解压--哈夫曼编码”

it2022-12-30  94

1、写在前面

文件的压缩原理与字符串压缩原理相同,都是用哈夫曼编码;文件的压缩与解压涉及到输入输出流操作;注意: 如果文件本身经过压缩处理,再使用哈夫曼编码压缩,效率不会有明显变化,如ppt,视频等文件;哈夫曼编码压缩按照字节处理,所以可以处理所有文件;哈夫曼编码的压缩率与文件中内容的重复率有关,重复的内容越多,压缩率越高。

2、Java代码 -- 文件的解压和压缩

package DataStructures.Tree; import java.io.*; import java.util.*; /** * @author yhx * @date 2020/10/17 */ public class HuffmanCode { /** * 生成哈夫曼树对应的哈夫曼编码 * 1、将哈夫曼编码存放在 Map<Byte,String> 中 * 2、生成哈夫曼编码时,需要拼接路径,定义一个StringBuilder存储某个叶子节点的路径 */ static Map<Byte, String> huffmanCodes = new HashMap<>(); static StringBuilder stringBuilder = new StringBuilder(); public static void main(String[] args) { String srcFile = "C://Users//bigyang//Desktop//==//JavaWorkspace//测试文件.txt"; String dstFile = "C://Users//bigyang//Desktop//==//JavaWorkspace//压缩后的文件.zip"; zipFile(srcFile, dstFile); System.out.println("文件压缩完成"); String zipFile = "C://Users//bigyang//Desktop//==//JavaWorkspace//压缩后的文件.zip"; String dstFile2 = "C://Users//bigyang//Desktop//==//JavaWorkspace//解压后的文件.txt"; unZipFile(zipFile,dstFile2); System.out.println("文件解压完成"); } /** * 接收字符串数组,转换为哈夫曼节点集合,节点统计了每个字符出现的次数 * * @param bytes 字符串数组 * @return 返回哈夫曼节点集合 */ private static List<HuffmanNode> getNodes(byte[] bytes) { // 1、创建一个ArrayList List<HuffmanNode> nodes = new ArrayList<>(); /* 2、统计每一个字符出现的次数 键Key:字符,byte整型 值Value:次数,Integer整型 */ HashMap<Byte, Integer> counts = new HashMap<>(); for (byte ch : bytes) { // 获取每个字符在哈希表中值(次数) Integer count = counts.get(ch); // 如果值为空,说明该字符第一次被统计到,将该字符ch(Key)的次数count(Value)改为1 if (count == null) { counts.put(ch, 1); } // 值不为空,说明该字符不是第一次出现,只需将出现次数加1 else { counts.put(ch, count + 1); } } // 3、遍历哈希表,将每个“键值对”转成一个HuffmanNode对象,加入到Nodes集合中 for (Map.Entry<Byte, Integer> entry : counts.entrySet()) { nodes.add(new HuffmanNode(entry.getKey(), entry.getValue())); } return nodes; } /** * 创建哈夫曼树 * * @param nodes 节点集合 * @return 返回树根节点 */ private static HuffmanNode createHuffmanTree(List<HuffmanNode> nodes) { while (nodes.size() > 1) { // 排序 Collections.sort(nodes); // 取出最小的二叉树 HuffmanNode leftNode = nodes.get(0); // 取出第二小的二叉树 HuffmanNode rightNode = nodes.get(1); // 创建一棵新的二叉树,没有data,只有权值 HuffmanNode parent = new HuffmanNode(null, leftNode.weight + rightNode.weight); parent.left = leftNode; parent.right = rightNode; // 移除两棵处理过的二叉树 nodes.remove(leftNode); nodes.remove(rightNode); // 将新的二叉树加入到nodes集合 nodes.add(parent); } // 最后的节点就是哈夫曼树的根节点 return nodes.get(0); } /** * 得到 node节点的所有叶子节点的哈夫曼编码,并存放到容器huffmanCodes中 * * @param node 传入的节点(根节点) * @param code 叶子节点的路径。左子节点是0,右子节点是1 * @param stringBuilder 拼接路径 */ private static void getCodes(HuffmanNode node, String code, StringBuilder stringBuilder) { StringBuilder stringBuilder2 = new StringBuilder(stringBuilder); // 将code拼接到stringBuilder2后 stringBuilder2.append(code); if (node != null) { // 判断当前node节点是否是叶子节点 if (node.data == null) { // 左右递归处理 getCodes(node.left, "0", stringBuilder2); getCodes(node.right, "1", stringBuilder2); } // 叶子节点 else { // 说明找到了某个叶子节点,将对应的路径(哈夫曼编码)存入到容器huffmanCodes中 huffmanCodes.put(node.data, stringBuilder2.toString()); } } } /** * 为了调用方便,重载getCodes方法 * * @param root 根节点 * @return 返回哈夫曼编码表 */ private static Map<Byte, String> getCodes(HuffmanNode root) { if (root == null) { return null; } // 处理根节点的左右子树 getCodes(root.left, "0", stringBuilder); getCodes(root.right, "1", stringBuilder); return huffmanCodes; } /** * 输入一个字符串数组,返回哈夫曼编码压缩后的 byte[] * * @param bytes 待压缩字符串数组 * @param huffmanCodes 生成的哈夫曼编码表 * @return 返回压缩结果 */ private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) { // 先利用huffmanCodes将字符数组bytes转为哈夫曼编码对应的字符串 StringBuilder stringBuilder = new StringBuilder(); for (byte b : bytes) { // 按顺序得到每个字符对应的哈夫曼编码,并将其拼接到一起 stringBuilder.append(huffmanCodes.get(b)); } //System.out.println("5、根据哈夫曼编码表,将字符串转为二进制编码字符串:\n" + stringBuilder.toString() + "\n"); // 统计返回byte[]的长度 // 以下6行代码等价于:int len = (stringBuilder.length() + 7) / 8; int len; // 因为每8位对应一个byte,所以步长为8 int stride = 8; if (stringBuilder.length() % stride == 0) { len = stringBuilder.length() / stride; } else { len = stringBuilder.length() / stride + 1; } // 存储压缩后的byte数组 byte[] huffmanCodeBytes = new byte[len]; // 记录是第几个byte int index = 0; for (int i = 0; i < stringBuilder.length(); i += stride) { String strByte; // 不足8位 if (i + stride > stringBuilder.length()) { strByte = stringBuilder.substring(i); } // 位数足够 else { strByte = stringBuilder.substring(i, i + stride); } // 将strByte转成一个byte,放入到huffmanCodeBytes数组中 // parseInt(String s, int radix)函数:表示将s字符串(radix表示其进制)转为10进制整型 huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2); index++; } return huffmanCodeBytes; } /** * 将以上方法封装,传入待压缩字符串,返回压缩后的哈夫曼编码字节数组 * * @param contentBytes 待压缩字符串 * @return 哈夫曼编码 */ private static byte[] huffmanZip(byte[] contentBytes) { // 将每个字符转为node节点 List<HuffmanNode> nodes = getNodes(contentBytes); // 创建哈夫曼树 HuffmanNode huffmanTreeRoot = createHuffmanTree(nodes); //System.out.println("3、哈夫曼树前序遍历的结果:"); //huffmanTreeRoot.preOrder(); //System.out.println(); // 根据哈夫曼树创建哈夫曼编码表 //System.out.println("4、生成哈夫曼编码表:"); Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot); //System.out.println(huffmanCodes); System.out.println(); // 根据哈夫曼编码表将字符串压缩为哈夫曼编码字节数组 byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes); //System.out.println("6、字符串转为哈夫曼编码字节数组:\n" + Arrays.toString(huffmanCodeBytes) + "\n"); // 返回 return huffmanCodeBytes; } /** * 将一个byte转成一个二进制字符串,如 8 --> 0000 1000 * * @param b 字节 * @param flag 标识是否需要补高位,true表示需要。 * 如果是最后一个字节,不需要补高位。 * 因为最后一个字节在压缩时不一定有8位,解码时如果强行补位就改变其值了。 * @return 二进制字符串,按照补码返回 */ private static String byteToBitString(boolean flag, byte b) { int temp = b; if (flag) { /* 按位或,用于将正数补高位 256 二进制 => 1 0000 0000 */ temp = temp | 256; } String str = Integer.toBinaryString(temp); if (flag) { // 取后八位 return str.substring(str.length() - 8); } else { return str; } } /** * 解码 * * @param huffmanCodes 哈夫曼编码表 * @param huffmanBytes 哈夫曼编码得到的字节数组 * @return 原来字符串对应的数组 */ private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) { // 先得到哈夫曼编码字节数组对应的二进制字符串 StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < huffmanBytes.length; i++) { // 判断是否是最后一个字节 boolean flag = (i == huffmanBytes.length - 1); stringBuilder.append(byteToBitString(!flag, huffmanBytes[i])); } //System.out.println("7、(哈夫曼字节数组 -> 二进制编码字符串)解码后:\n" + stringBuilder.toString() + "\n"); // 解码 Map<String, Byte> map = new HashMap<>(); // 将哈夫曼编码表的键K值V反转 for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) { map.put(entry.getValue(), entry.getKey()); } //System.out.println("8、哈夫曼编码表的键K值V反转后为:\n" + map + "\n"); // 将二进制编码字符串转换为字符数组 List<Byte> list = new ArrayList<>(); for (int i = 0; i < stringBuilder.length(); ) { // 递增计数器 int count = 1; boolean flag = true; Byte b = null; while (flag) { // 递增取字符串中的0或1 String key = stringBuilder.substring(i, i + count); // 从编码表中找,是否有字符串对应的数值(ASCII) b = map.get(key); // 没有匹配到 if (b == null) { count++; } else { flag = false; } } // 将匹配到的数值(ASCII)添加到集合 list.add(b); i += count; } // 将集合中的数据放入数组,返回结果 byte[] bs = new byte[list.size()]; for (int i = 0; i < bs.length; i++) { bs[i] = list.get(i); } return bs; } /** * 文件压缩 * * @param srcFile 传入待压缩文件的全路径 * @param dstFile 压缩后的存放位置 */ private static void zipFile(String srcFile, String dstFile) { // 创建文件输入流和输出流 FileInputStream is = null; OutputStream os = null; ObjectOutputStream oos = null; try { is = new FileInputStream(srcFile); os = new FileOutputStream(dstFile); oos = new ObjectOutputStream(os); // 创建一个和源文件大小一致的byte[] byte[] b = new byte[is.available()]; // 读取文件 is.read(b); // 获取文件对应的哈夫曼编码表 byte[] huffmanBytes = huffmanZip(b); // 将哈夫曼编码表以对象流的形式输出出去,恢复源文件时会用到 oos.writeObject(huffmanBytes); // 将哈夫曼编码表写入压缩文件 oos.writeObject(huffmanCodes); } catch (Exception e) { System.out.println(e.getMessage()); } // 关闭文件 finally { try { is.close(); os.close(); oos.close(); } catch (Exception e) { System.out.println(e.getMessage()); } } } /** * 文件解压 * * @param zipFile 准备解压的文件 * @param dstFile 将文件解压的路径 */ private 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[]) ois.readObject(); // 读取哈夫曼编码表 Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject(); // 解码 byte[] bytes = decode(huffmanCodes, huffmanBytes); // 写入到目标文件 os = new FileOutputStream(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 root 根节点 */ private static void preOrder(HuffmanNode root) { if (root != null) { root.preOrder(); } else { System.out.println("哈夫曼树为空"); } } } /** * 创建节点Node,带数据和权值 */ class HuffmanNode implements Comparable<HuffmanNode> { Byte data; int weight; HuffmanNode left; HuffmanNode right; /** * 构造器 * * @param data 以ASCII码存放数据,如'a' = 97 * @param weight 权值,表示字符出现的次数 */ public HuffmanNode(Byte data, int weight) { this.data = data; this.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(HuffmanNode o) { // 升序排序 return this.weight - o.weight; } /** * 重写方法:打印节点信息 * * @return 打印形式 */ @Override public String toString() { return "Huffman Node [ data = " + data + " weight = " + weight + " ]"; } }

3、运行结果

测试文件是一个txt文件,内容如下,其中含有大量1,对于哈夫曼编码压缩,文件中重复的内容越多,压缩率越高;

程序运行后,将 “测试文件.txt” 压缩为 “压缩后的文件.zip” ,之后又将 “压缩后的文件.zip” 解压为 “解压后的文件.txt”;

最新回复(0)