之前利用赫夫曼树来对字符串进行压缩,相当于对字符串加密。现在需要利用赫夫曼编码来进行解码,也就是解密。
如果还不了解赫夫曼编码的小伙伴可以先看上篇文章——利用哈夫曼树实现哈夫曼编码进行字符串压缩
之前利用赫夫曼编码得到了字符串的byte数组,现在首先需要将byte数组中的值转为二进制并且还要转成补码(因为计算机中存的都是二进制补码)
转为二进制的代码如下(有注释):
/**
* 将十进制数转为至少八位的二进制数
*
* @param i 需要转换的十进制数
* @param flag 作为是否要补到8位的标志,true表示要补全
* @return 将二进制按字符串形式返回
*/
public static StringBuffer tenToBinary(int i, boolean flag) {
//拼接二进制
StringBuffer stringBuffer1 = new StringBuffer();
StringBuffer stringBuffer2 = new StringBuffer();
int temp;
//先存储绝对值
int cur = Math.abs(i);
//得到二进制的反序,之后直接翻转字符串就可以了
while (cur != 0) {
temp = cur % 2;
cur = cur / 2;
stringBuffer1.append(temp);
}
//如果不足八位而且不是数组中的最后一个值则补0
while (stringBuffer1.length() < 8 && flag) {
stringBuffer1.append(0);
}
//如果是负数,需要把最高位改成符号位
if (i < 0) {
String substring = stringBuffer1.substring(0, stringBuffer1.length() - 1);
stringBuffer2.append(substring).append(1);
} else {
stringBuffer2 = stringBuffer1;
}
//翻转字符串,之前得到是二进制的反序
stringBuffer2 = stringBuffer2.reverse();
//返回得到的二进制字符串
return stringBuffer2;
}
再得到二进制的补码:
/**
* 得到十进制数对应的二进数的补码
*
* @param b 十进制数
* @param flag 作为是否要补到8位的标志,true表示要补全
* @return 字符串形式的二进制补码
*/
public static String byteToBitString(byte b, boolean flag) {
int temp = b;
//将temp转为二进制
StringBuffer strBinary = tenToBinary(temp, flag);
//测试
//System.out.println(strBinary);
// 如果是负数,得到temp对应的二进制补码,正数三码合一
if (temp < 0) {
//先转为反码
for (int i = 1; i < strBinary.length(); i++) {
if (strBinary.charAt(i) == '0') {
strBinary = strBinary.replace(i, i + 1, "1");
} else {
strBinary = strBinary.replace(i, i + 1, "0");
}
}
//再加一变成补码
int i = strBinary.length() - 1;
//如果是1就需要进位,再置为0
while (strBinary.charAt(i) == '1') {
strBinary = strBinary.replace(i, i + 1, "0");
i--;
}
//最后别忘了将0改为1
strBinary = strBinary.replace(i, i + 1, "1");
}
return strBinary.toString();
}
然后就需要循环将byte数组中的所有数的二进制补码都拿到并且存在可拼接的字符串中,
得到的就是之前用赫夫曼编码压缩成的二进制数
代码如下:
/**
* 将压缩后的编码解开,也就是解码
*
* @param huffmanCodes 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @param strSize 字符串经过赫夫曼压缩后的大小
* @return 就是原来的字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes,int strSize) {
StringBuffer stringBuffer1 = new StringBuffer();
//解码
for (int i = 0; i < huffmanBytes.length; i++) {
//如果是最后一个值并且二进制个数不能被8整除则不需要补全
if (i == huffmanBytes.length - 1 && strSize % 8 != 0)
stringBuffer1.append(byteToBitString( huffmanBytes[i],false));
else
stringBuffer1.append(byteToBitString(huffmanBytes[i],true));
}
//测试
System.out.println("输出字符串解压成赫夫曼编码后对应的二进制编码:" + stringBuffer1 + "长度为:" + stringBuffer1.length());
Map<String ,Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> stringByteEntry:huffmanCodes.entrySet()) {
map.put(stringByteEntry.getValue(),stringByteEntry.getKey());
}
//测试
//System.out.println(map);
List<Byte> list = new ArrayList<>();
for (int i = 0; i < stringBuffer1.length();) {
int count = 0;
Byte b = null;
//扫描字符串
while (true){
//递增的取出key
String key = "";
key = stringBuffer1.substring(i,i+count);
// 如果map集合中没有对应的二进制编码就count递增
b = map.get(key);
if (b == null){
//说明没取到
count++;
}else {
break;
}
}
list.add(b);
i += count;
}
//循环结束后将list集合中的字符串存储到byte数组中,并返回
byte b[] = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
将需要的所有方法封装在一个方法中,便于main方法调用
/**
* 将之前写的方法封装在一个方法之中,便于调用
*
* @param str 原始的字符串
* @return 经过赫夫曼编码压缩后对应的字节数组
*/
public static void huffmanAll(String str) {
// 把输入的字符串转为byte数组,在byte数组中存储的是字符对应的ASCII码值
byte[] strBytes = str.getBytes();
System.out.println(str + ",压缩成赫夫曼编码前对应的byte数组:" + Arrays.toString(strBytes));
//计算压缩前的字符串有多少位二进制数
int compressionBeforeCodeSize = str.length() * 8 + str.length() - 1;
System.out.println(str + ",压缩前的字符串大小:" + compressionBeforeCodeSize);
//统计字符串中每个字符出现的次数和空格出现次数并存入Node节点中
List<Node> nodeList = totalCharCounts(str);
//创建huffman树
Node root = createHuffmanTree(nodeList);
//得到压缩后的编码
getHuffmanCompressionCode(root, "", stringBuffer);
//输出赫夫曼编码表
System.out.println(str + ",对应的赫夫曼编码表:");
System.out.println(huffmanCodes);
//得到压缩后的字符串大小
int compressionAfterCodeSize = getStrCodeSize();
System.out.println(str + ",压缩后的字符串大小:" + compressionAfterCodeSize);
//可以算出压缩率是多少
double compressionRadio = (compressionBeforeCodeSize - compressionAfterCodeSize) * 1.0 / compressionBeforeCodeSize;
System.out.println(str + ",压缩成赫夫曼编码的压缩率为:" + compressionRadio);
byte[] bytes = zip(strBytes, huffmanCodes);
//解码
byte[] decodeByte = decode(huffmanCodes, bytes,compressionAfterCodeSize);
System.out.println("解码后的字符串为:" + new String(decodeByte));
return ;
}
以下是编码与解码的全部方法:
import java.util.*;
/**
* 实现huffman编码
*/
public class HuffmanCode {
//将赫夫曼编码表存放在Map<Byte,String>中
public static Map<Byte, String> huffmanCodes = new HashMap<>();
//需要定义一个StringBuffer来存储某个节点的路径对于的编码
public static StringBuffer stringBuffer = new StringBuffer();
//创建一个map,来保存每个字符以及他对应出现的次数
public static Map<Character, Integer> map = new HashMap<>();
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("输入字符串:");
//scanner.next()方法不能输入空格,例如输入: aaa bbb实际上只能接收到aaa,空格后面的字符串都接收不到
//所以需要用scanner,nextLine()方法来接收字符串
String str = scanner.nextLine();
//调用总的方法,直接输出解码后的值
huffmanAll(str);
}
/**
* 将压缩后的编码解开,也就是解码
*
* @param huffmanCodes 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @param strSize 字符串经过赫夫曼压缩后的大小
* @return 就是原来的字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes,int strSize) {
StringBuffer stringBuffer1 = new StringBuffer();
//解码
for (int i = 0; i < huffmanBytes.length; i++) {
//如果二进制位的个数能被8整除也
if (i == huffmanBytes.length - 1 && strSize % 8 != 0)
stringBuffer1.append(byteToBitString( huffmanBytes[i],false));
else
stringBuffer1.append(byteToBitString(huffmanBytes[i],true));
}
//测试
System.out.println("输出字符串解压成赫夫曼编码后对应的二进制编码:" + stringBuffer1 + "长度为:" + stringBuffer1.length());
Map<String ,Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> stringByteEntry:huffmanCodes.entrySet()) {
map.put(stringByteEntry.getValue(),stringByteEntry.getKey());
}
//测试
System.out.println(map);
List<Byte> list = new ArrayList<>();
for (int i = 0; i < stringBuffer1.length();) {
int count = 0;
Byte b = null;
//扫描字符串
while (true){
//递增的取出key
String key = "";
key = stringBuffer1.substring(i,i+count);
b = map.get(key);
if (b == null){
//说明没取到
count++;
}else {
break;
}
}
list.add(b);
i += count;
}
//循环结束后将list集合中的字符串存储到byte数组中,并返回
byte b[] = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
/**
* 得到十进制数对应的二进数的补码
*
* @param b 十进制数
* @param flag 作为是否要补到8位的标志,true表示要补全
* @return 字符串形式的二进制补码
*/
public static String byteToBitString(byte b, boolean flag) {
int temp = b;
//将temp转为二进制
StringBuffer strBinary = tenToBinary(temp, flag);
//测试
//System.out.println(strBinary);
// 得到temp对应的二进制补码
if (temp < 0) {
//先转为反码
for (int i = 1; i < strBinary.length(); i++) {
if (strBinary.charAt(i) == '0') {
strBinary = strBinary.replace(i, i + 1, "1");
} else {
strBinary = strBinary.replace(i, i + 1, "0");
}
}
//再加一变成补码
int i = strBinary.length() - 1;
while (strBinary.charAt(i) == '1') {
strBinary = strBinary.replace(i, i + 1, "0");
i--;
}
strBinary = strBinary.replace(i, i + 1, "1");
}
return strBinary.toString();
}
/**
* 将十进制数转为至少八位的二进制数
*
* @param i 需要转换的十进制数
* @param flag 作为是否要补到8位的标志,true表示要补全
* @return 将二进制按字符串形式返回
*/
public static StringBuffer tenToBinary(int i, boolean flag) {
//拼接二进制
StringBuffer stringBuffer1 = new StringBuffer();
StringBuffer stringBuffer2 = new StringBuffer();
int temp;
int cur = Math.abs(i);
while (cur != 0) {
temp = cur % 2;
cur = cur / 2;
stringBuffer1.append(temp);
}
//如果不足八位则补0
while (stringBuffer1.length() < 8 && flag) {
stringBuffer1.append(0);
}
if (i < 0) {
String substring = stringBuffer1.substring(0, stringBuffer1.length() - 1);
stringBuffer2.append(substring).append(1);
} else {
return stringBuffer1.reverse();
}
//翻转字符串,之前得到是二进制的反序
stringBuffer2 = stringBuffer2.reverse();
return stringBuffer2;
}
/**
* 将之前写的方法封装在一个方法之中,便于调用
*
* @param str 原始的字符串
* @return 经过赫夫曼编码压缩后对应的字节数组
*/
public static void huffmanAll(String str) {
// 把输入的字符串转为byte数组,在byte数组中存储的是字符对应的ASCII码值
byte[] strBytes = str.getBytes();
System.out.println(str + ",压缩成赫夫曼编码前对应的byte数组:" + Arrays.toString(strBytes));
//计算压缩前的字符串有多少位二进制数
int compressionBeforeCodeSize = str.length() * 8 + str.length() - 1;
System.out.println(str + ",压缩前的字符串大小:" + compressionBeforeCodeSize);
//统计字符串中每个字符出现的次数和空格出现次数并存入Node节点中
List<Node> nodeList = totalCharCounts(str);
//创建huffman树
Node root = createHuffmanTree(nodeList);
//得到压缩后的编码
getHuffmanCompressionCode(root, "", stringBuffer);
//输出赫夫曼编码表
System.out.println(str + ",对应的赫夫曼编码表:");
System.out.println(huffmanCodes);
//得到压缩后的字符串大小
int compressionAfterCodeSize = getStrCodeSize();
System.out.println(str + ",压缩后的字符串大小:" + compressionAfterCodeSize);
//可以算出压缩率是多少
double compressionRadio = (compressionBeforeCodeSize - compressionAfterCodeSize) * 1.0 / compressionBeforeCodeSize;
System.out.println(str + ",压缩成赫夫曼编码的压缩率为:" + compressionRadio);
byte[] bytes = zip(strBytes, huffmanCodes);
//解码
byte[] decodeByte = decode(huffmanCodes, bytes,compressionAfterCodeSize);
System.out.println("解码后的字符串为:" + new String(decodeByte));
return ;
}
/**
* @return 得到压缩后的赫夫曼编码大小
*/
public static int getStrCodeSize() {
int size = 0;
//将两个map集合都转为set集合
Set<Map.Entry<Character, Integer>> mapSet = map.entrySet();
Set<Map.Entry<Byte, String>> huffmanMapSet = huffmanCodes.entrySet();
//循环两个set集合
for (Map.Entry<Character, Integer> set1 : mapSet) {
for (Map.Entry<Byte, String> set2 : huffmanMapSet) {
//如果两个set的key相同就将他们的value相乘,只是需要注意存储huffman编码中的是字符串,需要乘字符串的长度
if ((byte) set1.getKey().charValue() == set2.getKey()) {
size = size + set1.getValue() * (set2.getValue().length());
//节约时间,之间退出内循环。因为不可能有一对多的关系。
break;
}
}
}
return size;
}
/**
* 根据huffman树来进行数据编码压缩
* 思路:
* 1、只要向左子树走就代表0,向右子树走就代表1
* 2、从头节点走到对于字符在的节点位置的路径对于的0和1组成的二进制编码就是压缩后该字符对于的编码
* 3、需要定义一个StringBuffer来存储某个节点的路径对于的编码
* 4、将赫夫曼编码表存放在Map<Byte,String>中
*
* @param node huffman树的根节点
* @param stringBuffer 用于拼接路径
* @param code 路径:左子节点是0,右子节点是1
* @return
*/
private static void getHuffmanCompressionCode(Node node, String code, StringBuffer stringBuffer) {
StringBuffer stringBuffer1 = new StringBuffer(stringBuffer);
stringBuffer1.append(code);
//如果为空,不进行处理
if (node != null) {
//判断node是叶子节点还是非叶子节点
if (node.data == null) {
//非叶子节点
//向左递归
getHuffmanCompressionCode(node.left, "0", stringBuffer1);
//向右递归
getHuffmanCompressionCode(node.right, "1", stringBuffer1);
} else {
//叶子节点
//说明这条路走到尾了,将路径编码存入map中
huffmanCodes.put(node.data, stringBuffer1.toString());
}
}
}
/**
* //统计字符串中每个字符出现的次数和空格出现次数
*
* @param str 字符串
* @return 返回一个排好序的Node集合
*/
public static List<Node> totalCharCounts(String str) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
Integer count = map.get(ch);
if (count == null) {
count = 0;
}
map.put(ch, count + 1);
}
//遍历map,将map中的数据存入Node节点中
//先将map转为set集合
Set<Map.Entry<Character, Integer>> mapSet = map.entrySet();
//观察测试输出
//System.out.println(mapSet);
List<Node> nodeList = new ArrayList<>();
//遍历set
for (Map.Entry<Character, Integer> set : mapSet) {
// 将map中的数据存入Node节点中
Node node = new Node((byte) set.getKey().charValue(), set.getValue());
// 将node存入集合中
nodeList.add(node);
//System.out.println(set.getKey() + " = " + set.getValue());
}
//排序
Collections.sort(nodeList);
//测试
//System.out.println(nodeList);
return nodeList;
}
/**
* 创建huffman树
*
* @param nodeList 排好序的集合
* @return 返回huffman树的根节点
*/
public static Node createHuffmanTree(List<Node> nodeList) {
//循环创建huffman树
while (nodeList.size() > 1) {
//1、每次取出集合中的前两个节点
Node left = nodeList.get(0);
Node right = nodeList.get(1);
//2、将他们的权值相加构成一个新的节点并作为他们的父节点
Node parent = new Node(null, left.weight + right.weight);
parent.left = left;
parent.right = right;
//3、删除已经处理过的节点
nodeList.remove(left);
nodeList.remove(right);
//4、将新的节点存入集合中
nodeList.add(parent);
//5、重新给集合排序,循环这5步即可,直到集合中只有一个节点,这就是huffman树的根节点
Collections.sort(nodeList);
//观察测试输出
//System.out.println(nodeList);
}
//返回huffman树的根节点
return nodeList.get(0);
}
/**
* 编写一个方法,将字符串对应的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();
* 返 回 的 是 字 符 串:
* "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000
* 101111111100110001001010011011100"
* => 对应的 byte[] huffmanCodeBytes ,即 8 位对应一个 byte,放入到 huffmanCodeBytes
* huffmanCodeBytes[0] = 10101000(补码) => byte [推导 10101000=> 10101000 - 1 => 10100111(反
* 码)=> 11011000(源码) = -88 ]
*/
public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//1、先利用赫夫曼编码表将传进来的bytes数组转为压缩后的编码
StringBuffer stringBuffer1 = new StringBuffer();
for (byte b : bytes) {
stringBuffer1.append(huffmanCodes.get(b));
}
//输出字符串压缩成赫夫曼编码后对应的二进制编码
System.out.println("输出字符串压缩成赫夫曼编码后对应的二进制编码:" + stringBuffer1 + "长度为:" + stringBuffer1.length());
//获取byte数组的长度,Math.ceil()表示向上取整
int len = (int) Math.ceil(stringBuffer1.length() * 1.0 / 8);
//也可以用下面的方法获取长度
/*if(stringBuffer1.length() % 8 == 0) {
len = stringBuffer1.length() / 8;
} else {
len = stringBuffer1.length() / 8 + 1;
}*/
//测试
//System.out.println(stringBuffer1.length());
//System.out.println(len);
byte[] huffmanBytes = new byte[len];
int index = 0;
for (int i = 0; i < stringBuffer1.length(); i = i + 8) {
String strByte;
if (i + 8 > stringBuffer1.length()) {
//从i取到字符串最后一个字符
strByte = stringBuffer1.substring(i);
} else {
//一次截取8个
strByte = stringBuffer1.substring(i, i + 8);
}
//将 strByte 转成一个 byte,放入到 huffmanBytes中
//该方法是将strByte对应的01字符串传换为十进制
//第二个参数表示基数(radix),表示转换为radix进制
huffmanBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffmanBytes;
}
}
以下是我的测试结果,有些情况会出现bug(实在改不出来了),比如字符串中不能有中文
1、输入的字符串为:i like like like java do you like a java
2、输入的字符串为: hello world java