哈夫曼树
Huffman 编码问题
问题引入
什么是编码?
简单说就是建立【字符】到【数字】的对应关系,如下面大家熟知的 ASC II 编码表,例如,可以查表得知字符【a】对应的数字是十六进制数【0x61】
\ | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0a | 0b | 0c | 0d | 0e | 0f |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0000 | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0a | 0b | 0c | 0d | 0e | 0f |
0010 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 1a | 1b | 1c | 1d | 1e | 1f |
0020 | 20 | ! | " | # | $ | % | & | ’ | ( | ) | * | + | , | - | . | / |
0030 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? |
0040 | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
0050 | P | Q | R | S | T | U | V | W | X | Y | Z | [ | \ | ] | ^ | _ |
0060 | ` | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
0070 | p | q | r | s | t | u | v | w | x | y | z | { | | | } | ~ | 7f |
注:一些直接以十六进制数字标识的是那些不可打印字符
传输时的编码
- java 中每个 char 对应的数字会占用固定长度 2 个字节
- 如果在传输中仍采用上述规则,传递 abbccccccc 这 10 个字符
- 实际的字节为 0061006200620063006300630063006300630063(16进制表示)
- 总共 20 个字节,不经济
现在希望找到一种最节省字节的传输方式,怎么办?
假设传输的字符中只包含 a,b,c 这 3 个字符,有同学重新设计一张二进制编码表,见下图
- 0 表示 a
- 1 表示 b
- 10 表示 c
现在还是传递 abbccccccc 这 10 个字符
- 实际的字节为 01110101010101010 (二进制表示)
- 总共需要 17 bits,也就是 2 个字节多一点,行不行?
不行,因为解码会出现问题,因为 10 会被错误的解码成 ba,而不是 c
- 解码后结果为 abbbababababababa,是错误的
怎么解决?必须保证编码后的二进制数字,要能区分它们的前缀(prefix-free)
用满二叉树结构编码,可以确保前缀不重复
- 向左走 0,向右走 1
- 走到叶子字符,累计起来的 0 和 1 就是该字符的二进制编码
再来试一遍
- a 的编码 0
- b 的编码 10
- c 的编码 11
现在还是传递 abbccccccc 这 10 个字符
- 实际的字节为 0101011111111111111(二进制表示)
- 总共需要 19 bits,也是 2 个字节多一点,并且解码没有问题了,行不行?
这回解码没问题了,但并非最少字节,因为 c 的出现频率高(7 次)a 的出现频率低(1 次),因此出现频率高的字符编码成短数字更经济
考察下面的树
- 00 表示 a
- 01 表示 b
- 1 表示 c
现在还是传递 abbccccccc 这 10 个字符
- 实际的字节为 000101 1111111 (二进制表示)
- 总共需要 13 bits,这棵树就称之为 Huffman 树
- 根据 Huffman 树对字符和数字进行编解码,就是 Huffman 编解码
Huffman 树
public class HuffmanTree {
Node root;
String code;
private static class Node{
char ch;
int freq;
String code;
Node left;
Node right;
public Node(char ch) {
this.ch = ch;
}
public Node(char ch, int freq) {
this.ch = ch;
this.freq = freq;
}
public Node(int freq, Node left, Node right) {
this.freq = freq;
this.left = left;
this.right = right;
}
public boolean isLeaf(){
return this.left == null && this.right == null;
}
}
public HuffmanTree(String s){
char[] charArray = s.toCharArray();
Map<String,Integer> map = new HashMap();
for (char c : charArray) {
Integer i = map.getOrDefault(String.valueOf(c),0);
map.put(String.valueOf(c),i+1);
}
PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingInt(v -> v.freq));
for (String string : map.keySet()) {
Node node = new Node(string.charAt(0), map.get(string));
queue.add(node);
}
while(queue.size() > 1){
Node n1 = queue.poll();
Node n2 = queue.poll();
Node node = new Node(n1.freq + n2.freq, n1, n2);
queue.add(node);
}
root = queue.peek();
s = doEncode(root,new StringBuilder(),s);
code = s;
}
}
Huffman 编解码
补充两个方法,注意为了简单期间用了编解码都用字符串演示,实际应该按 bits 编解码
public class HuffmanTree {
// ...
// 编码
private String doEncode(Node node,StringBuilder sb,String s){
if(!node.isLeaf()){
s = doEncode(node.left,sb.append(0),s);
sb.deleteCharAt(sb.length()-1);
s = doEncode(node.right,sb.append(1),s);
sb.deleteCharAt(sb.length()-1);
}else{
node.code = sb.toString();
while(s.contains(String.valueOf(node.ch))){
s = s.replace(String.valueOf(node.ch), node.code);
}
}
return s;
}
public String encode(){
return code;
}
public String decode(String code){
Node node = root;
StringBuilder sb = new StringBuilder();
char[] charArray = code.toCharArray();
for (int i = 0; i < charArray.length; i++) {
if(charArray[i] == '0'){
node = node.left;
}else {
node = node.right;
}
if(node.isLeaf()){
sb.append(node.ch);
node = root;
}
}
return sb.toString();
}
public static void main(String[] args) {
HuffmanTree tree = new HuffmanTree("aabcccccc");
String encode = tree.encode();
System.out.println(encode);
String decode = tree.decode(encode);
System.out.println(decode);
}
}