简介
哈夫曼编码(霍夫曼编码)是由美国计算机科学家 David Albert Huffman 于 1952 年发明的数据压缩算法
压缩原理
现实中我们看到的字符编码都是等长编码,例如常见的ASCII编码大小为一个字节(8bit),但一个字符使用
的频率有高低,使用等长编码就造成了浪费,哈夫曼编码就是一种变长编码算法,根据字符出现频率的不同,对应的编码
长度也不一样,规律是频率越高,字符编码长度越短,总体的平均编码长度小于等于等长编码
例如,一条英文语句使用了10种英文字符,使用ASCII编码每个字符占用的大小为 8bit, 而使用哈夫曼编码每个字符的平均编码
长度为 4bit,生出来编码长度就是压缩空间
实现原理
哈夫曼编码
编码过程本质是在构造哈夫曼树,哈夫曼树是带权路径长度最短的二叉树,构造过程如下
- 将要编码的字符加入队列,按出现的频率(权重)升序排序
- 取出两个频率最小的节点,构造哈夫曼树,父节点为频率只和
- 将父节点加入队列,并按频率升序排序
- 重复第二步和第三步,直到队列中只有一个节点,即为构造好的哈夫曼树
得到哈夫曼树,遍历哈夫曼树即可得到哈夫曼编码,本质是二叉树的深度遍历,唯一多出的步骤是向左遍历添加字符0, 向右遍历添加字符1,遇到叶子节点,停止添加,得到该字符的哈夫曼编码
哈夫曼解码
解码是哈夫曼树遍历的逆过程,遇到字符0选择左子节点,遇到字符1选择右子节点,直到遇到叶子节点,即可得到该编码对应的 字符
代码实现
哈夫曼树结构定义
abstract public class HuffmanTree implements Comparable<HuffmanTree> {
public final int frequency;
public HuffmanTree(int freq) {
frequency = freq;
}
//按频率升序排序
@Override
public int compareTo(HuffmanTree o) {
return frequency - o.frequency;
}
}
public class HuffmanLeaf extends HuffmanTree {
public final char value;
public HuffmanLeaf(int freq, char val) {
super(freq);
value = val;
}
}
public class HuffmanNode extends HuffmanTree {
public final HuffmanTree left, right;
public HuffmanNode(HuffmanTree left, HuffmanTree right) {
super(left.frequency + right.frequency);
this.left = left;
this.right = right;
}
}
编码与解码
public class HuffmanCode {
//构造哈夫曼树
public static HuffmanTree buildTree(int[] charFreqs) {
//本质是小顶堆
PriorityQueue<HuffmanTree> trees = new PriorityQueue<>();
//构造叶子结点
for (int i = 0; i < charFreqs.length; i++) {
if (charFreqs[i] > 0) {
trees.offer(new HuffmanLeaf(charFreqs[i], (char)i));
}
}
if(trees.size() < 0) return null;
//构造Huffman树,取出权重最小的两个叶子结点,构造树结构后再次加入小顶堆,再次取出,直到只有一棵树
while (trees.size() > 1) {
HuffmanTree a = trees.poll();
HuffmanTree b = trees.poll();
trees.offer(new HuffmanNode(a, b));
}
return trees.poll();
}
//哈夫曼编码
public static void huffmanEncode(HuffmanTree tree, StringBuffer prefix) {
if(tree == null) return;
//遇到叶子节点,停止遍历,打印编码值
if(tree instanceof HuffmanLeaf) {
HuffmanLeaf leaf = (HuffmanLeaf) tree;
System.out.println(leaf.value + "\t" + leaf.frequency + "\t" + prefix);
} else if (tree instanceof HuffmanNode) {
HuffmanNode node = (HuffmanNode) tree;
// 遍历左树
prefix.append('0');
huffmanEncode(node.left, prefix);
//回退一步,遍历另一边的分支
prefix.deleteCharAt(prefix.length() -1);
// 遍历右树
prefix.append('1');
huffmanEncode(node.right, prefix);
//回退一步,遍历另一边的分支
prefix.deleteCharAt(prefix.length() -1);
}
}
// 哈夫曼解码
public static void huffmanDecode(HuffmanTree temp, String code, int index, HuffmanTree tree) {
if(temp == null) throw new RuntimeException("输入了错误编码");
//遇到叶子节点,输出该编码对应的字符
if(temp instanceof HuffmanLeaf) {
HuffmanLeaf leaf = (HuffmanLeaf) temp;
System.out.print(leaf.value);
//走到叶子节点,临时节点恢复为根节点
temp = tree;
if(index < code.length()) {
huffmanDecode(temp, code, index, tree);
}
} else if (temp instanceof HuffmanNode) {
HuffmanNode node = (HuffmanNode) temp;
if('0' == code.charAt(index)) {
huffmanDecode(node.left, code, index+1, tree);
} else if('1' == code.charAt(index)) {
huffmanDecode(node.right, code, index+1, tree);
} else {
throw new RuntimeException();
}
}
}
public static void main(String[] args) {
String test = "this is an example for huffman encoding and decoding, very nice! ";
int[] charFreqs = new int[256];
//存储频率,索引即为字符本身
for (char c : test.toCharArray()) {
charFreqs[c] ++;
}
HuffmanTree tree = buildTree(charFreqs);
System.out.println("SYMBOL\tWEIGH\tTHUFFMAN CODE");
huffmanEncode(tree, new StringBuffer());
//解码后得到"today good!"
String code = "10101111011011010000111011111011111011110110110011100";
huffmanDecode(tree, code, 0, tree);
}
}
时间复杂度与空间复杂度
时间复杂度:O(nlogn),n 为编码字符的个数,一次堆排和遍历到叶子结点时间复杂度为 O(logn),n 个节点得到时间复杂度为 O(nlogn)
空间复杂度:O(n),n 个叶子节点,总共 2n-1 个节点
哈夫曼编码思维在日常生活中的实践
在我看来,哈夫曼编码本质就是将更高权重的事情以更高的效率和权重分配,以前上学的时,总是将时间均等的分配在 各科上,甚至是擅长哪个科目,就更爱花时间在这个科目上,现在想来应该优先将时间更多分配在那些提升空间大的科目上, 那会我有这意识,定能上清华(狗头)