数据结构之二叉树(六):数据压缩-哈夫曼编码
基本介绍:
哈夫曼树是可变字长编码(VLC)的一种,广泛应用于数据文件压缩,其压缩率通常在20%~90%之间。
-
1.通信领域的信息处理方式1----定长编码
以上一段英文包括空格共有40个字符,它们对应的Ascii码如下:
再将这些Ascii码转化为二进制,便得到了:
总长度为359,也就是说用定长编码发送这句话需要359的长度。 -
2.通信领域的信息处理方式2----变长编码
仍是这句话,我们计算在这句话中各个字符出现的个数:
d:1,y:1,u:1,v:2,o:2,l:4,e:4,i:5,a:5,(空格) :9
我们按照各个字符出现的次数进行编码,出现次数越多的,编码越小,比如空格出现了9次,编码则为0.
得到如下:
0=‘(空格)’,1=a,10=i,11=3,100=k,101=l,110=o,111=v,1000=j,1001=u,1010=y,1011=d
按照以上的规则规定的编码,我们再传输这句话时,编码就变成了:
10010110100…(省略不写了,反正大家懂)
这样的编码便有了问题,如1既可以是a,也可以看作是10即i,这样便产生了冲突,需要一定的方式解决
(字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码。这种问题在哈夫曼编码中得到了解决)
**
3.哈夫曼编码原理剖析:
**
1)仍是这句话,我们计算在这句话中各个字符出现的个数:
d:1,y:1,u:1,v:2,o:2,l:4,e:4,i:5,a:5,(空格) :9
2)接着,我们按照上面字符出现的次数构建一棵哈夫曼树,次数作为权值。
(构建哈夫曼树的方法在第五章中已经阐述,这里不再进行代码表示)
构建后的哈夫曼树如图所示:
3)根据上图构建的哈夫曼树,给各个字符,规定编码,向左的路径为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
4)按照上面的哈夫曼编码,
该字符串对应的编码为(无损压缩):
此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀,不会造成匹配的多义性。很好的解决了第二种方式的前缀冲突问题。长度为133。
注意事项:
这个哈夫曼树根据排序方法不同,构建的树可能不是完全一样的,(比如当树中多个节点权值相同的情况),这样对应的哈夫曼编码也是不完全相同的。 但wpl(树的带权路径长度)是一样的,都是最小的。
代码示例:
1)创建节点Node(data(数据:存放每个字符对应的Acsii码值),weight(权值:每个字符出现的次数),left,right)
2)得到:"i like like like java do you like a java"对应的byte[]数组
3)编写一个方法,将准备构建哈夫曼树的Node节点放到List,形式(Node[data=97,weitht=5],Node(data=32,weifght=9),…)
4)通过list构建哈夫曼树
总结后的步骤便是:
1.通过getBytes()方法将字符串转化为字节数组
2.通过getNodes(bytes)方法将字节数组构建成哈夫曼树的叶子节点
3.通过createHuffmanTree(nodes)方法将叶子节点生成哈夫曼树。
4.通过getCodes(HuffmanRoot)方法利用哈夫曼树创建字符串对应的哈夫曼编码表
5.通过zipHuff(bytes,huffmanCodes)方法利用哈夫曼编码表将字符串转化成二进制形式,再将二进制转换成对应的字节数组。
参考代码:
package Tree06;
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) {
String str="i like like like java do you like a java";
byte[] contentBytes=str.getBytes();
System.out.println("未压缩前的长度:"+contentBytes.length);
byte[] huffmanZip = huffmanZip(contentBytes);
System.out.println("将字符串转换成功后得到的byte数组"+Arrays.toString(huffmanZip));
}
/***
* 生成哈夫曼编码的总方法
*
*/
private static byte[] huffmanZip(byte[] bytes) {
//利用传进来的bytes构建哈夫曼树的节点
List<Node> nodes=getNodes(bytes);
//利用构建的节点生成哈夫曼树,返回该树的根节点
Node HuffmanRoot=createHuffmanTree(nodes);
//利用getCodes方法获取每个字符对应的哈夫曼编码,存入到huffmanCodes编码表中
getCodes(HuffmanRoot);
//利用得到的huffman编码表将对应的字符串转换为字节码数组
byte[] zipHuff = zipHuff(bytes,huffmanCodes);
return zipHuff;
}
/**
* 将字符串对应的byte[]数组,通过生成的哈夫曼编码表,生成哈夫曼编码
* @param bytes 原始的字符串对应的byte[](contentBytes)
* @param huffmanCodes生成的哈夫曼编码表
* @return 返回字符串对应的哈夫曼编码
* 此方法的目的是将目的字符串转换成对应的哈夫曼编码后,每八位一个字节装入byte数组里去
*/
public static byte[] zipHuff(byte[] bytes,Map<Byte,String>huffmanCodes) {
//1.利用huffmanCodes将bytes转换成对应的字符串
StringBuilder stringBuilder=new StringBuilder();
for(byte b:bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
//统计返回byte[] huffmanCodeBytes长度
int len;
if(stringBuilder.length()%8==0) {
len=stringBuilder.length()/8;
}else {
len=stringBuilder.length()/8+1;
}
byte[] huffmanCodeBytes=new byte[len];
int index=0;
for(int i=0;i<stringBuilder.length();i+=8) {
String strByte;
if(i+8>stringBuilder.length()) {
strByte=stringBuilder.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>形式:字符的ascii码->对应的哈夫曼编码
* 2.拼接路径,利用StringBuilder存储某个叶子节点的路径
* @param root
*/
static Map<Byte,String> huffmanCodes=new HashMap<Byte,String>();
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);
//处理右子树
getCodes(root.right,"1",stringBuilder);
return huffmanCodes;
}
//生成传入的node节点的所有叶子节点的哈夫曼编码,并放入集合,code:路径
private static void getCodes(Node node,String code,StringBuilder stringBuilder) {
StringBuilder stringBuilder2=new StringBuilder(stringBuilder);
stringBuilder2.append(code);
if(node!=null) {
if(node.data==null) {
//递归处理
getCodes(node.left,"0",stringBuilder2);
getCodes(node.right,"1",stringBuilder2);
}else {
//说明是一个叶子节点
//表示找到某个叶子节点
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
//前序遍历
public static void frontShow(Node root) {
if(root!=null) {
root.frontShow();
}else {
System.out.println("树为空");
}
}
private static List<Node> getNodes(byte[] bytes){
//创建List
ArrayList<Node> nodes=new ArrayList<Node>();
//得到:"i like like like java do you like a java"对应的byte[]数组
Map<Byte,Integer> counts=new HashMap<>();
for(byte b:bytes) {
Integer count=counts.get(b);
if(count==null) {
counts.put(b,1);
}else {
counts.put(b, count+1);
}
}
//把每一个键值对转成一个Node对象,并加入到nodes集合
for(Map.Entry<Byte,Integer> entry:counts.entrySet()) {
nodes.add(new Node(entry.getKey(),entry.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.remove(leftNode);
nodes.remove(rightNode);
//加入新的
nodes.add(parent);
}
return nodes.get(0);
}
}
//创建Node
class Node implements Comparable<Node>{
Byte data;//存放数据:字符对应的ascii码值
int weight;//权值,字符出现的次数
Node left;
Node right;
public Node(Byte data, int weight) {
super();
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 frontShow() {
System.out.println(this);
if(this.left!=null) {
this.left.frontShow();
}
if(this.right!=null) {
this.right.frontShow();
}
}
}
输出结果: