数据结构笔记_33 赫夫曼解码

解压,涉及到Java基础,还有二进制的内容,代码还是有一定难度的。

下面先说一下补码:

补码:正数的补码是其本身,负数的补码是其取反加一。

对于人来说,做加减法时,会先考虑其符号,而对于计算机来说,如果让计算机也先判断符号位再分别做加或者减的运算的话,会使得计算机的电路设计变得较为复杂。所以,对于这种最基础、频繁的操作,在物理逻辑的设计上也要尽量简单。

补码应此需求而生,不管是加法还是减法,都统一用加法来做运算,使计算机的设计变得更加简单了。

一、分布代码

1、完成对压缩数据的解码部分代码:

	/**
	 * @param huffmanCodes 哈夫曼编码表 map
	 * @param huffmanBytes 哈夫曼编码得到的字节数组:[-88, -65, -56...]
	 * @return 原来的字符串对应的数组
	 */
	private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
    
    

		// 1.先得到huffmanBytes 对应的二进制的字符串,形式:"101010001..."
		StringBuilder stringBuilder = new StringBuilder();
		// 2.将byte[] 转成二进制的字符串
		for (int i = 0; i < huffmanBytes.length; i++) {
    
    
			byte b = huffmanBytes[i];
			// 判断是不是最后一个字节
			boolean flag = (i == huffmanBytes.length - 1);
			stringBuilder.append(byteToBitString(!flag, b));
		}
		System.out.println("赫夫曼字节数组转对应的二进制字符串:" + stringBuilder.toString());
		return null;
	}

测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2、反向查询:

在这里插入图片描述
输出:
在这里插入图片描述

3、匹配!

在这里插入图片描述

完整代码:

package com.huey.huffmancode;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HuffmanCode {
    
    

	public static void main(String[] args) {
    
    
		// TODO Auto-generated method stub
		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));
		System.out.println("压缩后长度:" + huffmanCodesBytes.length);

		// 测试一把byteToString方法
		// System.out.println(byteToBitString((byte) 1));
		byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
		System.out.println("原来的字符串:" + new String(sourceBytes));

		// 分布过程
		/*
		 * List<Node> nodes = getNodes(contentBytes); System.out.println("nodes=" +
		 * nodes);
		 * 
		 * // 测试创建的赫夫曼树 System.out.println("赫夫曼树"); Node huffmanTreeRoot =
		 * createHuffmanTree(nodes); System.out.println("前序遍历");
		 * huffmanTreeRoot.preOrder();
		 * 
		 * // 测试是否生成了对应的哈夫曼编码 Map<Byte, String> huffmanCodes =
		 * getCodes(huffmanTreeRoot); System.out.println("生成的哈夫曼编码表:" + huffmanCodes);
		 * 
		 * // 测试 byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
		 * System.out.println("huffmanCodeBytes=" +
		 * Arrays.toString(huffmanCodeBytes));// 17 //(40-17) / 40 = 0.575
		 * 
		 * //发送huffmanCodeBytes 数组
		 */

	}

	// 编写一个方法,完成对压缩数据的解码
	/**
	 * @param huffmanCodes 哈夫曼编码表 map
	 * @param huffmanBytes 哈夫曼编码得到的字节数组:[-88, -65, -56...]
	 * @return 原来的字符串对应的数组
	 */
	private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
    
    

		// 1.先得到huffmanBytes 对应的二进制的字符串,形式:"101010001..."
		StringBuilder stringBuilder = new StringBuilder();
		// 2.将byte[] 转成二进制的字符串
		for (int i = 0; i < huffmanBytes.length; i++) {
    
    
			byte b = huffmanBytes[i];
			// 判断是不是最后一个字节
			boolean flag = (i == huffmanBytes.length - 1);
			stringBuilder.append(byteToBitString(!flag, b));
		}

		// 把字符串按照指定的赫夫曼编码进行解码
		// 把赫夫曼编码表进行调换,因为要反向查询 a->100 100->?
		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) {
    
    
				// 1010100010111...
				// 递增取出 key 1 10 101
				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.将huffmanCodesBytes:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24,
	// -14, -117, -4, -60, -90, 28]
	// 2.重新转成赫夫曼编码对应的二进制字符串:"101010001011111111001000101111111100100010111111110010010100110111000
	// 1110000011011101000111100101000101111111100110001001010011011100" => 对照 赫夫曼编码
	// => "i like like like java do you like a java"
	/**
	 * 将一个byte 转成一个二进制的字符串
	 * 
	 * @param flag 标志是否需要补高位,如果是true表示需要补高位,如果是false表示不补,如果是最后一个字节,无需补高位
	 * @param b    传入的byte
	 * @return 是该b 对应的二进制的字符串(注意是按补码返回)
	 */
	private static String byteToBitString(boolean flag, byte b) {
    
    
		// Byte并无toBinaryString()方法,所以需要先转成int 的Integer
		// 使用一个变量保存 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);// 取倒数8位,substring(),参数是起始位置。
		} else {
    
    
			return str;
		}
	}

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

		return huffmanCodeBytes;
	}

	// 编写一个方法,将一个字符串对应的byte[],通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
	/**
	 * @param bytes        这是原始的字符串对应的byte[]
	 * @param huffmanCodes 生成的赫夫曼编码
	 * @return 举例:String content ="i like like like java do you like a java"; =>
	 *         byte[] contentBytes = content.getBytes();
	 *         注:存储在计算机里的都是补码,一个byte是8位,开头一个是符号位 返回的是:
	 *         字符串:"101010001011111111001000101111111100100010111111110010010100110111000
	 *         1110000011011101000111100101000101111111100110001001010011011100"
	 *         长度是133,正是上面content中字符串(含空格)的哈夫曼编码拼接后的字符长度。 对应的byte[] huffmanCodeBytes
	 *         即8位对应一个byte,放入到huffmanCodeBytes huffmanCodeBytes[0] = 10101000(补码) =>
	 *         [推导 10101000=>10101000-1=>10100111(反码)=>11011000 = -88(前面符号位为负)]
	 */
	private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
    
    
		// 1.利用huffmanCodeBytes将 bytes 转成 赫夫曼编码对应的字符串
		StringBuilder stringBuilder = new StringBuilder();
		// 2.遍历bytes 数组
		for (byte b : bytes) {
    
    
			stringBuilder.append(huffmanCodes.get(b));
		}
//		System.out.println("测试 stringBuilder=" + stringBuilder.toString());

		// 将"1010100..."转成 byte[]

		// 统计返回 byte [] huffmanCodeBytes 长度
		// 一句话:int len = (stringBuilder.length() + 7) / 8;
		int len;
		if (stringBuilder.length() % 8 == 0) {
    
    
			len = stringBuilder.length() / 8;
		} else {
    
    
			len = stringBuilder.length() / 8 + 1;
		}
		// 创建 存储压缩后的 byte 数组
		byte[] huffmanCodeBytes = new byte[len];
		int index = 0;// 记录是第几个byte
		for (int i = 0; i < stringBuilder.length(); i += 8) {
    
    // 因为是每8位对应一个byte,所以步长+8
			// 每循环一次,都取8位,放到strByte里
			String strByte;
			if (i + 8 > stringBuilder.length()) {
    
    // 不够8位
				strByte = stringBuilder.substring(i);// substring(i)有多少位就取多少位
			} else {
    
    
				strByte = stringBuilder.substring(i, i + 8);
			}
			// 将strByte 转成一个byte ,放入到huffmanCodeBytes
			huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
			index++;
		}
		return huffmanCodeBytes;
	}

	// 生成赫夫曼树对应的赫夫曼编码
	// 思路:
	// 1.将赫夫曼编码表存放在Map<Byte,String> 形式:32->01,97->100,100->11000等等[形式]
	static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
	// 2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径
	static StringBuilder stringBuilder = new StringBuilder();

	// 为了调用方便,我们重载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;
	}

	/**
	 * 功能:将传入的node结点的叶子节点的赫夫曼编码存放得到,并放入到HuffmanCodes集合
	 * 
	 * @param node          传入结点
	 * @param code          路径:左子节点是0,右子节点是1
	 * @param stringbuilder 用于拼接路径
	 */
	private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
    
    
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		// 将code 加入到 stringBuilder2
		stringBuilder2.append(code);// append:(文章后)增补
		if (node != null) {
    
    // 如果node == null 不处理
			// 判断node 是叶子节点还是非叶子结点
			if (node.data == null) {
    
    // 非叶子结点
				// 递归处理
				// 向左递归
				getCodes(node.left, "0", stringBuilder2);
				// 向右递归
				getCodes(node.right, "1", stringBuilder2);
			} else {
    
    // 说明是叶子节点
				// 就表示找到某个叶子节点的最后
				huffmanCodes.put(node.data, stringBuilder2.toString());
			}
		}
	}

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

	/**
	 * @param bytes 接收一个字节数组
	 * @return 返回的就是List形式[Node[data=97,weight = 5],Node[data=32,weight = 9]......]
	 */
	private static List<Node> getNodes(byte[] bytes) {
    
    
		// 1、创建一个ArrayList
		ArrayList<Node> nodes = new ArrayList<Node>();
		// 遍历 bytes,统计 每一个byte出现的次数 -> map[key,value]
		Map<Byte, Integer> counts = new HashMap<>();// 所谓映射,即给一个人编了个号,提到号就能知道是哪个人。
		// Map集合将键映射到值的对象,一个映射不能包含重复的键,每个键最多只能映射到一个值,
		// Map接口和Collection接口的不同,Map是双列的,Collection是单列的.
		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<Byte, Integer> entry : counts.entrySet()) {
    
    
			// for each循环,定义一个变量entry暂存集合中的每一个元素。循环counts.entrySet()中的每一个元素。
			// 其中,entrySet()表示返回Map.Entry对象(映射中的键/值对)的一个集视图(集合视图就是把集合
			// 里面的东西给你展示出来, 仅供查看, 而不能修改)。可以从这个集中删除元素,
			// 它们将从映射中删除,但是不能添加任何元素。

			// Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。
			// 它表示Map中的一个实体(一个key-value对)。接口中有getKey(),getValue方法。
			nodes.add(new Node(entry.getKey(), entry.getValue()));
			// getKey()、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;// 存放数据(字符)本身,比如'a' => 97 ' ' => 32
	int weight;// 权值,表示字符出现的次数
	Node left;
	Node right;

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

	@Override
	public int compareTo(Node o) {
    
    
		// TODO Auto-generated method stub
		// 从小到大排序
		return this.weight - o.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();
		}
	}

}

测试:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45909299/article/details/114445771
今日推荐