上一篇文章中提到数据结构:哈夫曼树
,今天接着学习由哈夫曼提出编码方式,一种程序算法。简称:哈夫曼编码
java
在线转码工具: https://www.mokuge.com/tool/a...node
与哈夫曼树同样,会不会有小伙伴对哈夫曼编码
很陌生,有疑惑算法
1.哈夫曼编码是作什么的?有什么用?
2.为何要使用哈夫曼编码?使用别的编码行不行?数组
它是基于二叉树构建
编码压缩结构
的,它是数据压缩
中经典的一种算法:根据文本字符出现的频率,从新对字符进行编码
数据结构
那么就会有小伙伴说:什么是数据压缩?什么是字符编码?
app
在咱们的世界里,咱们能够看到漂亮的图像、好听的声音、震撼的视频等等这些精彩内容。可是对于计算机
说,它的世界里只有二进制的 0 和 1
。ide
所以在数字电子电路中,逻辑门的实现直接应用了二进制,所以现代的计算机和依赖计算机的设备里都用到二进制。工具
那么在二进位电脑系统中,存储的单位有:bit、kb、mb、gb
学习
bit 电脑记忆体中最小的单位
,每一bit 只表明0 或 1
的数位讯号测试
Byte由八个 bits 所组成,可表明:字元(A~Z)、数字(0~9)、或符号(,.?!%&+-*/)
当记忆体容量过大时,位元组(byte)这个单位就不够用,所以就有千位元组的单位KB出现,如下乃个记忆体计算单位之间的相关性:
咱们如今在计算机上看到的一切的图像、声音、视频等内容,都是由二进制的方式进行存储
的
简单来说,咱们把信息转化为二进制的过程能够称之为编码
编码方式从长度上来分大体能够分为两个大类:
请注意!这里并无标明是多长!这会致使没法区分编码信息与文件内容的分离!形成乱码!
民可以使由之不可以使知之 ——_出自《论语 第八章 泰伯篇》_
这么一串十个字要如何去分隔并解释呢?
断法解释一:
民可以使由之,不可以使知之。解释:你能够去驱使你的民众,但不可以让他们知道为何(不要去教他们知识)
断法解释二:
民可,使由之;不可,使知之。解释:民众能够作的,放手让他们去作;不会作的,教会他们如何去作(又或:不能够去作的,让他们明白为什么不能够)
显然,以上的文字是以某种定长或变长的方式组合
在一块儿的,可是关于它们如何分隔的信息则被丢弃
了,因而在解释时就存在产生歧义可能
了。
举个栗子,假如咱们对 「pig
」 进行自定义编码,使用定长编码,为了方便,采用了十进制,原理与二进制是同样的。
假设咱们如今有文件,内容是:00080008
假如定长 2 位
是惟一的编码方案,用它去解码就会获得:「pipi」
假如定长 4 位
是惟一的编码方案,用它去解码就会获得:「ii」
若是文件的内容并无暗示它使用了何种编码
!就会出现解释时存在产生歧义
。
这就比如孔夫子写下“民可以使由之不可以使知之
”时
并无暗示它是5|5分隔
(民可以使由之|不可以使知之
)
仍是暗示它是2|3|2|3分隔
(民可|使由之|不可|使知之
)。
其实当咱们有字节这一基本单位后,咱们就能够说得更具体:如定长一字节或者定长二字节
。
好比说ASCII码它是最先也是最简单
的一种字符编码方案:使用定长一字节来表示一个字符
其实如今咱们的计算机的世界里编码总已经有不少种格式
好比常见的: ASCII 、 Unicode 、 GBK 、 GB2312 、 GB18030 、 UTF-8 、 UTF-16
等等。
咱们都知道在计算机的世界里只有二进制0和1
,经过不一样的排列组合
,可使用 0 和 1 就能够表示世界上全部的东西
。
是否是有咱们中国"太极"的感受。
——“太极生两仪,两仪生四象,四象生八卦
而在计算机中:一字节 = 八位二进制数(二进制有0和1两种状态)
所以一字节
能够组合出二百五十六
种状态。
若是这二百五十六种状态
每个都对应一个符号
,就能经过一字节
的数据表示二百五十六个字符
。
美国人因而就制定了一套编码(其实就是个字典),描述英语中的字符
和这八位二进制数的对应关系
,这被称为 ASCII 码。
随着时代的发展,不只美国人要对他们的英文进行编码,咱们中国人也要对汉字进行编码。
而早期的 ASCII 码的方案只有一个字节,对咱们汉字文化而言是远远不够的
这点可怜的空间拿到中国来,它能编码的汉字量也就小学一年级的水平。
这也就致使了不一样的需求推进了发展
,各类编码方案都出来了,彼此之间的转换也带来了诸多的问题
。采用某种统一的标准就势在必行了,因而乎天上一声霹雳,Unicode
登场!
Unicode早期是做为定长二字节方案出来的。它的理论编码空间一下达到了64k
不过对于能够只须要使用到一个字节的ASCII 码就能够表示的美国人来说,让他们使用 Unicode ,多少仍是不是很愿意的。
好比 「he」 两个字符,用 ASCII 只须要保存成 6865 ( 16 进制),如今则成了 00680065 ,前面多的那两个 0 (用做站位)。
基本上能够说是毫无心义,用 Unicode 编码的文本,原来可能只有 1KB 大小,如今要变成 2KB ,体积成倍的往上涨。
但是更糟糕的事还在后头,咱们中文但是字符集里面的大头。
1.“ 茴字有四种写法”——上大人孔乙己
2.听说有些新近整理的汉语字典收录的汉字数量已经高达10万级别!
若是仍是定长的方案,眼瞅着就要奔着四字节而去了,容量与效率的矛盾在这时候开始激化。
几个字节
表示一个字符
,显然用的字节越多,编码空间越大,能表示更多不一样的字符,也即容量越大。一个字符用
的字节越多
,所占用的存储空间也就越大
,换句话说,存储(乃至检索)的效率下降了。若是说效率是阴,那么容量就是阳。
(_我没还没忘记自小学语文老师就开始教导的,写做文要遥相呼应_)
看如图所示你就会发现,字母e只用了一个点(dot)来编码
其它字母可能以为不公平,为啥咱们就要录入那么多个点和划(dash)才行呢?这里面实际上是有统计规律支撑的。e出现的几率是最大的
zoo大概不少人能想到,厉害一点可能还能想到zebra(斑马),Zuckerberg(扎克伯格),别翻字典!你还能想到更多不?
含有的 e的单词则多了去了。zebra中不就有个e吗,Zuckerberg中还两个e呢
因此咱们但愿频率越高的词,编码越短,这样最终才能最大化压缩存储文本数据的空间
。
上面提到常见的信息处理方式:定长编码与变长编码
假设当前有一句话:i like like like java do you like a java
(共40个字符,包括空格)
咱们发现最后存在计算机中(二进制)长度是:三百五十九
假设咱们将这串10010110100...
发给别人,可是没有说明解码的方式,这时就会出现解释时存在产生歧义
。
好比说可能翻译成:"a a a aa a "
,因此咱们须要说明字符编码不能是其余字符编码的前缀,即不能匹配重复的编码
。
相比定长编码二进制的长度:三百五十九
,哈夫曼二进制长度为:一百三十三
那么相比定长编码二进制的长度优化了多少呢?:(359-133)/359=62.9%
咱们再将哈夫曼二进制转码对应的byte字符
那么相比原字符长度优化了多少呢?:(40-17)/40 = 57.5%
一下将原字符40位变成17位, 这样的状况下,是否是咱们想要的
上面咱们采用"i like like like java do you like a java"
,作例子分析,那么咱们如今分析分析哈夫曼编码思路
"i like like like java do you like a java"
d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9
1.将数列进行从小到大排序
2.新数列的每一个数当作最简单根节点的二叉树
3.取出权值最小的两颗二叉树组成新的二叉树,为两最小的树之和
4.将新二叉树以跟节点的权重值再次从小到大排序,不断重复步骤
"i like like like java do you like a java"
对应byte[]数组体现获取各个字符对应的个数:d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9
好比说字符:a 应该是Node[data='a', weight= 5],data为何会是97而不是a?
由于底层:字母等于ASCII数字
那么第一步:建立节点Node{data:存放数据,weight:权重,left和right}
class Nodedata implements Comparable<Nodedata>{ Byte data; //存放数据(字符)自己,好比'a' =>97 '' =>32 int weight; //权值,表示字符出现的次数 Nodedata left; Nodedata right; public Nodedata(Byte data, int weight) { this.data = data; this.weight = weight; } @Override public int compareTo(Nodedata o) { return this.weight - o.weight; } @Override public String toString() { return "Nodedata[" +"data=" + data + ", weight=" + weight +']'; } }
第二步:获得"i like like like java do you like a java"对应byte[]数组
public static void main(String[] args) { //获得`"i like like like java do you like a java"`对应byte[]数组 String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println(contentBytes.length); }
第三步:编写方法,将准备构建赫夫曼树的Node节点放到List
private static List<Nodedata> getNodes(byte[] bytes){ //1.建立一个ArrayList List<Nodedata> nodeslist = new ArrayList<Nodedata>(); //2.遍历bytes,统计每个byte出现的次数->map[key,value] Map<Byte,Integer> map = new HashMap<>(); for(byte b :bytes){ Integer items = map.get(b); if(items == null){ map.put(b,1); }else{ map.put(b, items + 1); } } //3.把每一个键值对转为Node 对象,并放入nodeslist集合里 for(Map.Entry<Byte,Integer> temp :map.entrySet()){ nodeslist.add(new Nodedata(temp.getKey(),temp.getValue())); } return nodeslist; }
第四步:经过List建立对应的哈夫曼树
private static Nodedata createHaffman(List<Nodedata> nodedataList){ while(nodedataList.size() >1){ //排序从小到大 Collections.sort(nodedataList); /** * 操做思路 * 1.取出两个权值最小的节点二叉树 * 2.根据两个权值最小的二叉树构建成一个新的二叉树 * 3.删除原先两个权值最小的节点二叉树 * 4.将新的二叉树放入队列,并构建新的队列 * 5.新的队列进行从小到大排序 */ //取出第一个权值最小的二叉树 Nodedata leftNode = nodedataList.get(0); //取出第二个权值最小的二叉树 Nodedata rightNode = nodedataList.get(1); //根据两个权值最小的二叉树构建成一个新的二叉树 同时构建链接 Nodedata parentNode = new Nodedata(null,leftNode.weight + rightNode.weight); parentNode.left = leftNode; parentNode.right = rightNode; //删除原先两个权值最小的节点二叉树 nodedataList.remove(leftNode); nodedataList.remove(rightNode); //将新的二叉树放入队列,并构建新的队列 nodedataList.add(parentNode); } return nodedataList.get(0); }
检查是否Byte[]长度:40
public static void main(String[] args) { //获得`"i like like like java do you like a java"`对应byte[]数组 String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println("byte[]数组长度为:"+contentBytes.length); } 运行结果以下: byte[]数组长度为:40
检查字符个数:d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9
public static void main(String[] args) { //将准备构建哈弗曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = ..... List<Nodedata> datalist = getNodes(contentBytes); for(Nodedata data : datalist){ System.out.println(data); } } 运行结果以下: Nodedata[data=32, weight=9] Nodedata[data=97, weight=5] Nodedata[data=100, weight=1] Nodedata[data=101, weight=4] Nodedata[data=117, weight=1] Nodedata[data=118, weight=2] Nodedata[data=105, weight=5] Nodedata[data=121, weight=1] Nodedata[data=106, weight=2] Nodedata[data=107, weight=4] Nodedata[data=108, weight=4] Nodedata[data=111, weight=2]
Nodedata 节点添加前序遍历,添加哈弗曼树方法
class Nodedata implements Comparable<Nodedata>{ //省略实体类代码 //前序遍历方法 public void preOrder(){ System.out.println(this); if(this.left != null){ this.left.preOrder(); } if(this.right != null){ this.right.preOrder(); } } }
private static void preOrder(Nodedata root){ if(root != null){ root.preOrder(); }else{ System.out.println("哈弗曼树为空!"); } }
检查前序遍历哈弗曼树,查看是否图一致:40->17->8->4->4->2->2->9...
public static void main(String[] args) { //获取哈弗曼树的根节点 Nodedata root = createHaffman(datalist); //遍历哈弗曼树 preOrder(root); } 运行结果以下: Nodedata[data=null, weight=40] Nodedata[data=null, weight=17] Nodedata[data=null, weight=8] Nodedata[data=108, weight=4] Nodedata[data=null, weight=4] Nodedata[data=106, weight=2] Nodedata[data=111, weight=2] Nodedata[data=32, weight=9] Nodedata[data=null, weight=23] Nodedata[data=null, weight=10] Nodedata[data=97, weight=5] Nodedata[data=105, weight=5] Nodedata[data=null, weight=13] Nodedata[data=null, weight=5] Nodedata[data=null, weight=2] Nodedata[data=100, weight=1] Nodedata[data=117, weight=1] Nodedata[data=null, weight=3] Nodedata[data=121, weight=1] Nodedata[data=118, weight=2] Nodedata[data=null, weight=8] Nodedata[data=101, weight=4] Nodedata[data=107, weight=4]
那么咱们前面分析了如何将字符转为哈弗曼树
的思路与代码
下面看看如何将根据哈夫曼树
进行构成自定义哈夫曼编码
。
向左为0、向右为1
o: 0011 u: 11001 d: 11000 y: 11010 i: 101 a: 100 k: 1111 e: 1110 j: 0010 V: 11011 I: 000 (空格):01
]有没有发现哈弗曼树的节点编码它是前缀编码:不是是其余字符编码的前缀,即匹配不到重复的编码
。
'a'的路径是:1->1->0
使用Key:Value 键值
的方式存放对应的字符:路径;
好比:(a:1000)、(j:0010)、(o:0011)
data !=null
(属于叶子节点)字符v的路径
,发现左递归
不符合后还须要进行右递归
判断//将哈夫曼编码以Key:Vale形式存放 //好比说32->01 97->100 100->11000 等等[形式] static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>(); //2.在生成赫夫曼编码表示,须要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径 static StringBuilder stringBuilder = new StringBuilder(); /** * 1.功能:将传入的node结点的全部叶子结点的赫夫曼编码获得,并放入到huffmanCodes集合 * @param node 传入节点 * @param code 路径:左节点是0 右节点是1 * @param stringBuilder 用于拼接路径 */ private static void getCodes(Nodedata node,String code,StringBuilder stringBuilder){ StringBuilder str = new StringBuilder(stringBuilder); //将code加入到str 总 str.append(code); if(node!=null){ //字符特色:`data !=null `(属于叶子节点) if(node.data ==null){//(属于非叶子节点) //向左递归 getCodes(node.left,"0",str); //向右递归 getCodes(node.right,"1",str); }else{ //`使用Key:Value 键值`的方式存放对应的`字符:路径;`好比:`(a:110)、(j:0000)、(o:1000)` huffmanCodes.put(node.data,str.toString()); } } }
咱们优化一下代码,实现传入根节点就能够返回哈夫曼编码
private static Map<Byte,String> getCodes(Nodedata node){ //根节点为空则不作处理 if(node == null){ return null; } //向左递归 getCodes(node.left,"0",stringBuilder); //向右递归 getCodes(node.right,"1",stringBuilder); return huffmanCodes; }
咱们进行运行测试看看,是否欲思路一致生成编码
public static void main(String[] args) { //将哈夫曼树生成哈夫曼编码 //getCodes(root,"",stringBuilder); //执行优化代码 getCodes(root); } 运行结果以下: 生成的哈夫曼编码表:{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
注意:不一样的排序方式会生成不一样的哈弗曼树,所形成的哈夫曼编码也不同,但WPL是一致
咱们将"i like like like java do you like a java"
字符串,即生成对应的哈夫曼编码数据(以下)
"i like like like java do you like a java"
转为对应的Byte[]数组字符路径
private static void 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.printf("stringBuilder=" + stringBuilder.toString()); System.out.print("\n stringBuilder的长度=" + stringBuilder.length()); }
public static void main(String[] args) { //获得`"i like like like java do you like a java"`对应byte[]数组 String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println("byte[]数组长度为:"+contentBytes.length); //将准备构建哈弗曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = ..... List<Nodedata> datalist = getNodes(contentBytes); //获取哈弗曼树的根节点 Nodedata root = createHaffman(datalist); //将哈夫曼树生成哈夫曼编码Key:Value getCodes(root); //将Byte[]数组转为自定义的哈夫曼编码字符路径 zip(contentBytes,huffmanCodes); } 运行结果以下: byte[]数组长度为:40 stringBuilder=1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100 stringBuilder的长度=133
咱们将"i like like like java do you like a java"
转为编码串相比以前定长编码优化了多少呢?:(359-133)/359=62.9%
那么如何将长度一百三十三
的二进制串转为新的字符
呢?
在讲编码串进行补码->反码->原码构建新字符前,先解释了解了解不一样的进制
在计算机的世界中就分两种人:认识二进制和不认识二进制
上面咱们介绍到咱们看到的漂亮的图像、好听的声音、震撼的视频等等这些精彩内容。可是对于计算机
说,它的世界里只有二进制的 0 和 1
。
二进制、十进制、八进制、十六进制
咱们使用Byte字符的二进制作演示,首先咱们直观一点举例正数说明
以此类推....那么十进制转二进制该怎么作呢?来看看规律
有没有发现?从右开始往左是2^0(二的零次方)、2^1(二的一次方)、2^2(二的平方)、2^3(二的三次方)
根据前面知识点,你知道:01101110
的十进制是多少么?
那么咱们说Byte表示 -128 ~127 ,那么0和1 是怎么表示正数和负数的呢?
咱们如今知道二进制的最高位:0 正数 1-负数
,那么对于整数还要提提三个码:原码、反码、补码
那么咱们思考一下:在计算机中: -14 是什么样的呢?
接下来看看负数的解析图:十进制:1四、二进制:00001110
那么咱们如今反推一下:10111011 转为十进制是多少呢?
以补码方式存储
0 - 正数、1-负数
补码
的形式,那么思路反推:补码->反码->原码
接下来我相信你们都了解了什么是原码、反码、补码
了
上面咱们将字符串"i like like like java do you like a java"
转为了自定义的哈弗曼编码串
接下来咱们建立Byte[] huffmanCodeBytes
,处理哈夫曼编码串,以八位为一组对应一个Byte
放入huffmanCodeBytes中
每八位对应一个Byte
,因此133 % 8,使用循环每次 i + 8
index 记录对应的第几位Byte
(byte)Integer.parInt(string,radix)
转码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)); } //2.1010100010111111...思路:补码->反码->原码->转成Byte int len; if(stringBuilder.length() %8 == 0){ len = stringBuilder.length() /8; }else{ len = stringBuilder.length() /8 + 1; } //3.建立存储压缩后的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()) {//不够8位 strByte = stringBuilder .substring(i); }else { strByte = stringBuilder .substring(i, i + 8); } //将strByte转成一个byte ,放入到huffmanCodeBytes huffmanCodeBytes[index] = (byte)Integer .parseInt(strByte, 2); index++; } return huffmanCodeBytes; }
public static void main(String[] args) { //获得`"i like like like java do you like a java"`对应byte[]数组 String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println("byte[]数组长度为:"+contentBytes.length); //将准备构建哈弗曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = ..... List<Nodedata> datalist = getNodes(contentBytes); //获取哈弗曼树的根节点 Nodedata root = createHaffman(datalist); //将哈夫曼树生成哈夫曼编码 getCodes(root); byte[] huffmanCodeBy =zip(contentBytes,huffmanCodes); System.out.println(Arrays.toString(huffmanCodeBy)); } 运行结果以下: byte[]数组长度为:40 [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
那么相比原字符长度优化了多少呢?:(40-17)/40 = 57.5%
一下将原字符40位变成17位
根据前面的分析,咱们调用的方法比较多也比较臃肿,每次测试须要
如今呢,咱们将封装这些方法方便咱们调用,咱们的目的是获取到新字符组
//使用一个方法将须要的方法疯转起来,方便调用 private static byte[] haffmanZip(byte[] bytes){ //将准备构建哈弗曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = ..... List<Nodedata> datalist = getNodes(bytes); //根据字符出现次数做为权重,构建哈弗曼树 //获取哈弗曼树的根节点 Nodedata root = createHaffman(datalist); //根据哈夫曼树进行自定义哈夫曼编码实现 Map<Byte,String> huffmanCodes = getCodes(root); //将原字符的全部哈夫曼编码构建成编码串 //根据编码串进行补码->反码->原码构建新字符 byte[] huffmanCodeBy =zip(bytes,huffmanCodes); return huffmanCodeBy; }
public static void main(String[] args) { String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println("byte[]数组长度为:"+contentBytes.length); byte[] huffmanCodeBy =haffmanZip(contentBytes); System.out.println(Arrays.toString(huffmanCodeBy)); System.out.println("压缩后的byte[]数组长度为:"+huffmanCodeBy.length); }
运行结果以下: byte[]数组长度为:40 [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] 压缩后的byte[]数组长度为:17
咱们已经将"i like like like java do you like a java"
包括空格在内的四十个字符
压缩成[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
只有十七位的新字符。
那么假如咱们将这串新字符发给对方
对方如何解码成"i like like like java do you like a java"
呢?
[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
是怎么来的?
那么咱们如今获取到新字符后须要使用逆向思惟获取原补码
咱们可使用Integer.toBinaryString()方法将byte转成二进制
/** *将一个byte转成-个二进制的字符串 * @param b * @return */ private static String byteToBitString(byte b) { //使用变量保存b int temp = b;//将b转成int String str = Integer.toBinaryString(temp); return str; }
咱们使用 -1 进行测试这个方法看看
//测试一把byteToBitString方法 System.out.println(byteToBitString((byte)-1)); 运行结果以下: str=11111111111111111111111111111111
很遗憾,咱们发现返回的是int 的32位补码。注意是:补码
按理说咱们应该是从133 % 8获取的新字符,如今应该也是转为8位的补码,而不是32位
/** *将一个byte转成-个二进制的字符串 * @param b * @return */ private static String byteToBitString(byte b) { //使用变量保存b int temp = b;//将b转成int String str = Integer.toBinaryString(temp); return str.substring(str.length() -8); }
那么如今就能够了吗?其实并否则还有一个补位的问题!好比说我如今输入 1
当是正数的时候,当前stu =1 则须要补位、不然是没法完成截取操做的
/** *将一个byte转成-个二进制的字符串 * @param b * @return */ private static String byteToBitString(byte b) { //使用变量保存b int temp = b;//将b转成int //若是是正数咱们还存在补高位 temp |= 256; String str = Integer.toBinaryString(temp); return str.substring(str.length() -8); } 运行结果以下: 00000001
???
可能会有小伙伴会问到 temp |=256 是什么意思???为何要|=256
这就涉及到知识点二进制的运算符了,两个二进制对应位为0时该位为0,不然为1
这样咱们输入十进制:1 的时候,才能进行截取长度,不然不知足
/** *将一个byte转成-个二进制的字符串 * @param flag 标志是否须要补高位、若是是最后一个字节则无需补高位 * @param b 传入的byte * @return 返回b对应的二进制串(按补码返回) */ private static String byteToBitString(boolean flag,byte b) { //使用变量保存b int temp = b;//将b转成int if(flag){ //若是是正数咱们还存在补高位 temp |= 256; } String str = Integer.toBinaryString(temp); if(flag){ return str.substring(str.length() -8); }else{ return str; } }
根据八位为一组的思路,就会发现最后一个byte字节就无需补高位
第一步:使用StringBuilder记录新字符组的补码
/** * @param huffmanCodes 哈夫曼编码表map * @param huffmanBytes 哈夫曼获得的字节数组 * @return就是原来的字符串对应的数组 */ private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){ //1.使用StringBuilder记录新字符组的补码 StringBuilder stringBuilder =new StringBuilder(); //循环记录 for (int i=0;i<huffmanBytes.length;i++){ byte b = huffmanBytes[i]; //最后一个字符无需补位 boolean flag = (i == huffmanBytes.length -1?true:false); stringBuilder.append(byteToBitString(!flag,b)); } System.out.println("新字符组的补码串:"+stringBuilder.toString()); System.out.println("新字符组的补码串长度:"+stringBuilder.length()); return null; }
public static void main(String[] args) { /获得`"i like like like java do you like a java"`对应byte[]数组 String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println("原byte[]数组长度为:"+contentBytes.length); byte[] huffmanCodeBy =haffmanZip(contentBytes); System.out.println("n开始进行哈夫曼编码压缩==============================n"); System.out.println("压缩后的新字符数组:"+Arrays.toString(huffmanCodeBy)); System.out.println("压缩后的byte[]数组长度为:"+huffmanCodeBy.length); decode(huffmanCodes,huffmanCodeBy); } 运行结果以下: 原byte[]数组长度为:40 原字符数组通过自定义编码后的编码串:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100 原字符数组通过自定义编码后的编码串长度:133 开始进行哈夫曼编码压缩============================== 压缩后的新字符数组:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] 压缩后的byte[]数组长度为:17 新字符组的补码串:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100 新字符组的补码串长度:133
第二步:找到记录原字符byte转为自定义哈夫曼编码Map、根据自定义哈夫曼编码Map反获取(编码:字符)组成新map
/** * * @param huffmanCodes 哈夫曼编码表map * @param huffmanBytes 哈夫曼获得的字节数组 * @return就是原来的字符串对应的数组 */ private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){ //1.使用StringBuilder记录新字符组的补码 StringBuilder stringBuilder =new StringBuilder(); //循环记录 for (int i=0;i<huffmanBytes.length;i++){ byte b = huffmanBytes[i]; //最后一个字符无需补位 boolean flag = (i == huffmanBytes.length -1?true:false); stringBuilder.append(byteToBitString(!flag,b)); } //找到记录原字符byte转为自定义哈夫曼编码Map、根据自定义哈夫曼编码Map反获取原字符 Map<String,Byte> map =new HashMap<String, Byte>(); for(Map.Entry<Byte,String> item:huffmanCodes.entrySet()){ map.put(item.getValue(),item.getKey()); } System.out.println("根据自定义哈夫曼编码Map反获取(编码:字符)组成新map = "+map); return null; } 运行结果以下: 根据自定义哈夫曼编码Map反获取(编码:字符)组成新map = {000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}
第三步:根据StringBuilder匹配新map获取对应字符
/** * * @param huffmanCodes 哈夫曼编码表map * @param huffmanBytes 哈夫曼获得的字节数组 * @return就是原来的字符串对应的数组 */ private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){ //1.使用StringBuilder记录新字符组的补码 StringBuilder stringBuilder =new StringBuilder(); //循环记录 for (int i=0;i<huffmanBytes.length;i++){ byte b = huffmanBytes[i]; //最后一个字符无需补位 boolean flag = (i == huffmanBytes.length -1?true:false); stringBuilder.append(byteToBitString(!flag,b)); } //找到记录原字符byte转为自定义哈夫曼编码Map、根据自定义哈夫曼编码Map反获取原字符 Map<String,Byte> map =new HashMap<String, Byte>(); for(Map.Entry<Byte,String> item:huffmanCodes.entrySet()){ map.put(item.getValue(),item.getKey()); } //根据StringBuilder匹配新map获取对应字符 List<Byte> list =new ArrayList<>(); for (int j=0;j<stringBuilder.length();){ int count = 1; boolean flag = true; Byte b = null; while(flag){ String key =stringBuilder.substring(j,j+count); b = map.get(key); if(b == null){ count++; }else{ flag = false; } } list.add(b); j +=count; } //当for循环结束后,list就存放了字符串"i like like like java do you like a java"的全部字符 //把集合里的数据放入byte[]中返回 byte[] b = new byte[list.size()]; for (int k =0;k<b.length; k++){ b[k]=list.get(k); } return b; }
public static void main(String[] args) { /获得`"i like like like java do you like a java"`对应byte[]数组 String content = "i like like like java do you like a java"; byte[] contentBytes = content.getBytes(); System.out.println("原byte[]数组长度为:"+contentBytes.length); byte[] huffmanCodeBy =haffmanZip(contentBytes); System.out.println("n开始进行哈夫曼编码压缩==============================n"); System.out.println("压缩后的新字符数组:"+Arrays.toString(huffmanCodeBy)); System.out.println("压缩后的byte[]数组长度为:"+huffmanCodeBy.length); byte[] source = decode(huffmanCodes,huffmanCodeBy); System.out.println("新字符数组通过根据新map获取对应字符:"+new String(source)); } 运行结果以下: 根据自定义哈夫曼编码Map反获取(编码:字符)组成新map = {000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106} 新字符数组通过根据新map获取对应字符:i like like like java do you like a java
咱们将这个图片文件,进行压缩实践看看
思路:读取文件-->获得哈夫曼编码表-->完成压缩
/** *@paramsrcFile 但愿压缩文件的全路径 *@param dstFile 压综后将文件放到哪一个目录 */ public static void zipFile(String srcFile, String dstFile) { //建立输出流 OutputStream os = null; //对象输出流 ObjectOutputStream oos; //建立文件的输入流 FileInputStream is = null; try { //建立文件的输入流 is = new FileInputStream(srcFile); //建立一个和源文 件大小同样的byte[ ] byte[] b = new byte[is.available()]; //读取文件 is.read(b); //直接对文件压缩 byte[] bytes = haffmanZip(b); //建立文件的输出流,存放压缩文件 os = new FileOutputStream(dstFile); //建立一个和文件流关联的ObjectOutputStream; oos = new ObjectOutputStream(os); //以对象流的方写入哈夫曼编码,为了方便恢复 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()); } } }
public static void main(String[] args) { //测试压编文件 String srcFile = "d://src. bmp"; String dstFile = "d://dst.zip"; zipFile(srcFile, dstFile); System. out . println("压编文件ok~~"); }
会出现没法打开并解压的方式,为何呢?
由于咱们自定义属于本身的的压缩方式,因此解压也要用咱们本身的方式去解压
源文件大小:598kb
压缩后的文件大小:76kb
那么接下来咱们编写本身的解压方式把
思路:读取压缩文件(数据和哈弗曼编码表)-->完成解压
//编写一个方法,完成对压缩文件的解压 /** * @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); //建立一个和文件流关联的ObjectOutputStream; ois = new ObjectInputStream(is); //读取byte数组 huffmanbytes byte[] huffmanBytes = (byte[])ois.readObject(); //获取哈夫曼编码表 Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject(); //解码 byte[] bytes =decode(huffmanCodes,huffmanBytes); //将恢复的原byte数组写入文件 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 e) { System.out.println(e.getMessage()); } } }
public static void main(String[] args) { //测试解压文件 String zipFile = "d://dst. zip"; String dstFile = "d://src2. bmp"; unZipFile(zipFile, dstFile) ; }