赫夫曼编码介绍
- 赫夫曼编码(Huffman Coding):属于一种算法。是赫夫曼树在电讯通信中的经典应用之一
- 赫夫曼编码广用于数据文件压缩,压缩率通常在%20 ~ %90之间,如果文本重复数据越多,那么压缩率越高
- 赫夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码
原理解析
在通常
- 通信领域信息的处理方式之一-定长编码:就是把所有包括空格符号在内的字符编译成Ascii,再转成二进制。如此总长度会比较长
- 通信领域信息的处理方式之二-变长编码:是按照各个字符出现的次数进行编码,原则是 出现的次数越多 编码值越小。如:[a b c c] 变长编码为:空格:0 c:1 b:1010 a:1011,空格出现的次数最多,而编码值最小。而编码相结合后得到的编码为:1011 0 1010 0 1 0 1,相对【定长编码】来说长度有所减少。但是该编码相结合后计算机很难看出里面的0和1的组合是否与别的字符编码前面的某几个数字重合,即不符合前缀编码(字符的编码都不能是其他字符编码的前缀)
- 赫夫曼编码原理解析:在拿到字符串后,首先统计出每个字符的数量,然后把每个字符出现的次数看做权值,如字符串:i ilke ilke ilke java do you ilke 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。 构建成一颗赫夫曼树,可以在赫夫曼树节点的基础属性中用一个char类型的变量保存字符或其它类型的其它内容。并且指定,向每个节点的左路径走编码为+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
如此一来所得到的每个字符编码和编码前缀都不会与别的字符编码的前缀重复,并且编译后的长度为133,比定长编码369的长度压缩了很多。其中要注意的是:如果有重复的权值,那么赫夫曼树排序的方法会不同,得到的赫夫曼树和编码也不大一样。但是不用担心,得到wpl是一样的,能够得到无损压缩并且长度一样。
代码实现
树节点的定义&前序遍历
1.首先我们要创建一个(节点类Node)定义树中节点的属性,属性有:存放字符数据变量,存放权值变量,左路径,右路径。
// 继承Comparable类,用于使用里面的排序方法
public class Node implements Comparable<Node> {
Byte data; // 这里使用Byte类型来存储字符,而每个字符都有对应的Ascii码值
int weight; // 存放权值,即字符出现的次数
Node left;
Node right;
// 构造器
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
// 使用Comparable里的方法进行升序排序
@Override
public int compareTo(Node o) {
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();
}
}
}
main方法&创建字符形式
2.再定义一个字符类型数组,用于装字符串中的每个字符
public static void main(String[] args) {
String content = "i ilke ilke ilke java do you ilke a java";
// 把每个字符方进
byte[] contentBytes = content.getBytes();
// 输出长度
System.out.println(contentBytes.length); // 40
}
字符转换节点保存到集合&创建相应赫夫曼树
将字符数组中的元素转化成节点,并保存到集合中:
1.1.首先定义一个ArrayList
1.2.然后定义一个Map的变量来接收HashMap类型数据,其中Byte类型表示为nodes数据内容,Integer表示为次数
2.1.然后用循环遍历字符数组(bytes)
2.2.循环体内先用前面声明的类型Integer定义变量count循环获取字符的出现次数
2.3.因为有可能获取到,有可能获取不到,所以再判断count是否等于空
为空的话表示Map还没有整个字符数据,则第一次添加次数为1:counts.put(b,1)
反之表示不是第一次则 再原来的基础上++
3.1.遍历完后把每个字符与属性内容转成节点 Node对象,并加入到nodes集合中
循环遍历Map,原理和 for(类型 下标变量 : 变量目标) 一样
public static ArrayList<Node> getNodes (byte[] bytes){
ArrayList<Node> nodes = new ArrayList<Node>(); // 1.1
Map<Byte,Integer> counts = new HashMap<>(); // 1.2
// 遍历每个字符
for(byte b: bytes) { // 2.1
Integer count = counts.get(b); // 2.2
//2.3
if(count == null) {
counts.put(b,1);
} else {
counts.put(b,count++);
}
}
// 3.1
for(Map.Entry<Byte, Integer> entry: counts.entrySet()) {
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
根据ArrayList集合创建一个对应的赫夫曼树
1.1因为数列里的节点一直相加 去除 新增 到最后只剩下最后的一个节点,就是根节点,所以使用while
循环的条件为:当集合的长度大于1时循环
1.2循环中先使用Collections.sort()方法进行升序排序
1.3然后定义节点变量leftNode与rightNode保存集合中的最小值和次小值,也就是排序后集合的第0个与第1个节点
1.4并创建 new出根节点parent,并且它的根节点只有权值没有字符或Ascii码,所以data部分为空,
权值为leftNode与rightNode权值相加:Node parent = new Node(null,leftNode.weight + rightNode.weight);
1.5并且根节点左右路径链接上leftNode与rightNode
1.6然后使用集合中的方法,从集合中去除掉处理过的节点,并加入相加出来的根节点parent到集合中
private static Node createHuffmanTree(ArrayList<Node> nodes) {
// 1.1
while(nodes.size() > 1) {
// 1.2
Collections.sort(nodes);
// 1.3
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
// 1.4
Node parent = new Node(null,leftNode.weight + rightNode.weight);
// 1.5
parent.left = leftNode;
parent.left = rightNode;
// 1.6
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent);
}
return nodes.get(0);
}