07赫夫曼编码
基本介绍
-
赫夫曼编码也翻译为 [哈夫曼]编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
-
赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
-
赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
-
赫夫曼码是可变[字长]编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码
原理剖析
通信领域中信息的处理方式1-定长编码
-
i like like like java do you like a java // 共40个字符(包括空格)
-
105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97 //对应ASCII码
-
01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应的二进制
-
按照二进制来传递信息,总的长度是 359 (包括空格)
通信领域中信息的处理方式2-变长编码
-
i like like like java do you like a java // 共40个字符(包括空格)
-
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 空格:9 // 各个字符对应的个数
-
0= 空格, 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d
说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推. -
按照上面给各个字符规定的编码,则我们在传输 "i like like like java do you like a java" 数据时,编码就是
-
10010110100...
-
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码(这个在赫夫曼编码中,我们还要进行举例说明, 不捉急)
通信领域中信息的处理方式3-赫夫曼编码
-
i like like like java do you like a java // 共40个字符(包括空格)
-
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 空格:9 // 各个字符对应的个数
-
•按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
步骤:
传输的 字符串
-
i like like like java do you like a java
-
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
-
按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
-
构成赫夫曼树的步骤:
- ①从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
- ②取出根节点权值最小的两颗二叉树
- ③组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- ④再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 ①②③④的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

-
根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为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
-
按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133
-
长度为 : 133
说明:
原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性赫夫曼编码是无损处理方案
代码思路:
功能: 根据赫夫曼编码压缩数据的原理,需要创建 "i like like like java do you like a java" 对应的赫夫曼树
思路:
(1) Node { data (存放数据), weight (权值), left 和 right }
(2) 得到 "i like like like java do you like a java" 对应的 byte[] 数组
(3) 编写一个方法,将准备构建赫夫曼树的Node 节点放到 List , 形式 [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......], 体现 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
(4) 可以通过List 创建对应的赫夫曼树
代码
- 压缩
//(1) Node { data (存放数据), weight (权值), left 和 right }
//创建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;
}
//前序遍历
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(Node o) {
return this.weight-o.weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
}
//(2) 编写一个方法(统计对应的字符出现的次数),将准备构建赫夫曼树的Node 节点放到 List , 形式[Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......], 体现 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
private static List<Node> getNodes(byte[] bytes){
//1创建一个Arraylistnodes
List<Node> nodes = new ArrayList<Node>();
//遍历 bytes , 统计 每一个byte出现的次数->map[key,value]
Map<Byte,Integer> counts = new HashMap<>();
for(byte bts:bytes){
Integer count = counts.get(bts);
if(count==null){// Map还没有这个字符数据,第一次
counts.put(bts,1);
}else{
counts.put(bts,count+1);
}
}
//把每一个键值对转成一个Node 对象,并加入到nodes集合
//遍历map
for(Map.Entry<Byte,Integer> entry : counts.entrySet()){
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
//(3) 可以通过List 创建对应的赫夫曼树(方法)
private static Node creatHt(List<Node> listnodes){
while(listnodes.size()>1){
//排序, 从小到大
Collections.sort(listnodes);
//取出第一颗最小的二叉树
Node leftnode = listnodes.get(0);
//取出第二颗最小的二叉树
Node rightnode = listnodes.get(1);
//创建一颗新的二叉树,它的根节点 没有data, 只有权值
Node parent = new Node(null,leftnode.weight+rightnode.weight);
parent.left=leftnode;
parent.right=rightnode;
//将已经处理的两颗二叉树从nodes删除
listnodes.remove(leftnode);
listnodes.remove(rightnode);
//将新的二叉树,加入到nodes
listnodes.add(parent);
}
//nodes 最后的结点,就是赫夫曼树的根结点
return listnodes.get(0);
}
//(4)创建对应的赫夫曼编码
//生成赫夫曼树对应的赫夫曼编码
//思路:
//1. 将赫夫曼编码表存放在 Map<Byte,String> 形式
// 生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
static Map<Byte, String> huffmanCodes = new HashMap<Byte,String>();
//2. 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
static StringBuilder stbr = new StringBuilder();
private static void getCodes(Node node, String code, StringBuilder stbr){
StringBuilder stbr2 = new StringBuilder(stbr);
//将code 加入到 stringBuilder2
stbr2.append(code);
if(node!=null){//如果node == null不处理
//判断当前node 是叶子结点还是非叶子结点
if(node.data==null){
//非叶子结点
//递归处理
//向左递归
getCodes(node.left,"0",stbr2);
//向右递归
getCodes(node.right,"1",stbr2);
}else{//说明是一个叶子结点
//就表示找到某个叶子结点的最后
huffmanCodes.put(node.data,stbr2.toString());
}
}
}
//(5)生成哈夫曼编码对应的字符数组
//编写一个方法,将字符串对应的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();
* 返回的是 字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
* => 对应的 byte[] huffmanCodeBytes ,即 8位对应一个 byte,放入到 huffmanCodeBytes
* huffmanCodeBytes[0] = 10101000(补码) => byte [推导 10101000=> 10101000 - 1 => 10100111(反码)=> 11011000= -88 ]
* huffmanCodeBytes[1] = -88
* 计算机中存储是以补码格式存储的,只有转成原码才能算出对应的十进制数
*/
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());
//将 "1010100010111111110..." 转成 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
String stbryte;
if(i+8>stringBuilder.length()){//不够8位
stbryte = stringBuilder.substring(i);
}else{
stbryte = stringBuilder.substring(i,i+8);
}
//将stbryte 转成一个byte,放入到 huffmanCodeBytes
huffmanCodeBytes[index]=(byte) Integer.parseInt(stbryte,2);
index++;
}
return huffmanCodeBytes;
}
//(6)所有方法进行封装
private static byte[] huffmanZip(byte[] bytes) {
List<Node> nodes = getNodes(bytes);
//根据 nodes 创建的赫夫曼树
Node huffmanTreeRoot = creatHt(nodes);
//对应的赫夫曼编码(根据 赫夫曼树)
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
//根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
return huffmanCodeBytes;
}
//-------------------------------------------------
//解压缩
//-------------------------------------------------
/**
完成数据的解压
思路
1. 将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
重写先转成 赫夫曼编码对应的二进制的字符串 "1010100010111..."
2. 赫夫曼编码对应的二进制的字符串 "1010100010111..." =》 对照 赫夫曼编码 =》 "i like like like java do you like a java"
*/
//(1)将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]重写先转成 赫夫曼编码对应的二进制的字符串"1010100010111..."
/**
* 将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
* @param b 传入的 byte
* @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
* @return 是该b 对应的二进制的字符串,(注意是按补码返回)
*/
private static String bytrToString(boolean flag,byte b){
//使用变量保存 b
int temp = b;
//如果是正数我们还存在补高位
if(flag){
temp |=256; //按位与 256 1 0000 0000 | 0000 0001 => 1 0000 0001
}
String str = Integer.toBinaryString(temp);
if(flag){
return str.substring(str.length()-8);//返回的是temp对应的二进制的补码
}else{
return str;
}
}
//(2)编写一个方法,完成对压缩数据的解码
/**
*
* @param huffmanCodes 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 就是原来的字符串对应的数组
*/
public static byte[] decode(Map<Byte,String>huffmanCodes,byte[]huffmanBytes){
//1. 先得到 huffmanBytes 对应的 二进制的字符串 , 形式 1010100010111...
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(bytrToString(!flag,b));
}
// System.out.println(stringBuilder.toString());
//把字符串安装指定的赫夫曼编码进行解码
//把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
Map<String,Byte> convertmap = new HashMap<String,Byte>();
for(Map.Entry<Byte,String> map:huffmanCodes.entrySet()){
convertmap.put(map.getValue(),map.getKey());
}
// System.out.println(convertmap);
//创建要给集合,存放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
String key = stringBuilder.substring(i,i+count);//i 不动,让count移动,指定匹配到一个字符
b = convertmap.get(key);
if(b==null){
//说明没有匹配到
count++;
}else{
//匹配到
flag=false;
}
}
list.add(b);
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;
}
//编写方法,将一个文件进行压缩
//编写方法,将一个文件进行压缩
/**
*
* @param srcFile 你传入的希望压缩的文件的全路径
* @param dstFile 我们压缩后将压缩文件放到哪个目录
*/
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();
oos.close();
os.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
//编写一个方法,完成对压缩文件的解压
/**
*
* @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);
//创建一个和 is关联的对象输入流
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);
//将bytes 数组写入到目标文件
os = new FileOutputStream(dstFile);
//写数据到 dstFile 文件
os.write(bytes);
}catch (Exception e)
{
// TODO: handle exception
System.out.println(e.getMessage());
}finally {
try {
os.close();
ois.close();
is.close();
} catch (Exception e2) {
// TODO: handle exception
System.out.println(e2.getMessage());
}
}
}
- 测试
public static void main(String[] args) {
//压缩测试
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
byte[] convertbytes = huffmanZip(contentBytes);
System.out.printf(Arrays.toString(convertbytes)+ "\n" + "长度="+convertbytes.length);
//解压缩测试
byte[] decode = decode(huffmanCodes, convertbytes);
System.out.println(new String(decode));
/**文件压缩测试*/
String srcFile = "F:\\Java_Learning\\Files\\cat.jpg";
String dstFile = "F:\\Java_Learning\\Files\\cat.zip";
ZipFile(srcFile,dstFile);
System.out.println("压缩文件成功!");
/**文件解压测试*/
String zipFile = "F:\\Java_Learning\\Files\\cat.zip";
String dstFile = "F:\\Java_Learning\\Files\\cat2.jpg";
unZipFile(zipFile, dstFile);
System.out.println("解压成功!");
}
输出:
[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
长度=17
i like like like java do you like a java
赫夫曼编码压缩文件注意事项
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化,比如视频,ppt等等文件[举例压一个.ppt]
- 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件)[举例压一个.xml文件]
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显.

浙公网安备 33010602011771号