霍夫曼编码(Huffman coding)是一种常用的数据压缩算法,它是由David A. Huffman在1952年提出的。该算法通过使用变长编码来表示不同的字符,使出现频率较高的字符拥有较短的编码,而出现频率较低的字符拥有较长的编码,从而实现数据的高效压缩。
以下是霍夫曼编码的基本原理和步骤:
- 统计字符频率:首先,需要统计待压缩数据中每个字符的出现频率。这个频率信息将用于构建霍夫曼树。
- 构建霍夫曼树:使用字符频率信息构建霍夫曼树。霍夫曼树是一种二叉树,其中字符频率较高的字符位于树的较低层,而频率较低的字符位于较高层。构建过程通常是通过不断合并两个频率最低的节点来进行的,直到只剩下一个根节点。这个过程保证了较常见的字符拥有较短的编码,而较不常见的字符拥有较长的编码。
- 分配编码:从霍夫曼树的根节点开始,向左走表示0,向右走表示1,从根节点到每个叶子节点的路径即为字符的编码。这些编码是变长的,这是霍夫曼编码的一个关键特点,因为不同字符的编码长度不同。
- 压缩数据:使用生成的霍夫曼编码替换原始数据中的字符,将数据压缩为较短的比特流。这个过程可以显著减小数据的大小,特别是对于包含大量重复字符的数据。
- 解压数据:要解压数据,需要使用相同的霍夫曼树来将编码重新映射为原始字符。
霍夫曼编码的优势在于它能够根据字符的频率动态地分配编码,以使常见字符的编码较短,从而实现高效的数据压缩。这种编码方式在无损压缩领域广泛应用,如在图像、音频和文本数据的压缩中,例如JPEG图像压缩、MP3音频压缩以及ZIP文件压缩。
需要注意的是,霍夫曼编码是一种无损压缩算法,这意味着压缩和解压后的数据是精确一致的,不会丢失信息。
public class HuffmanTree {
static class Node {
Character ch; //字符
int freq;//频次
Node left;
Node right;
String code; //编码
public Node(Character ch) {
this.ch = ch;
}
public Node(int freq, Node left, Node right) {
this.freq = freq;
this.left = left;
this.right = right;
}
public Node(Character ch, int freq, Node left, Node right, String code) {
this.ch = ch;
this.freq = freq;
this.left = left;
this.right = right;
this.code = code;
}
public int freq() {
return freq;
}
boolean isLeaf() {
return left == null;
}
@Override
public String toString() {
return "Node{" +
"ch=" + ch +
", freq=" + freq +
'}';
}
}
String str;
Map<Character, Node> map = new HashMap<>();
Node root;
public HuffmanTree(String str) {
this.str = str;
char[] chars = str.toCharArray();
//功能1:统计频率
for (char c : chars) {
// if (!map.containsKey(c)) {
// map.put(c, new Node(c));
// }
// Node node = map.get(c);
// node.freq++;
Node node = map.computeIfAbsent(c, Node::new);
node.freq++;
}
//功能2,构造树
PriorityQueue<Node> queue = new PriorityQueue<>(
Comparator.comparingInt(Node::freq)
);
queue.addAll(map.values());
while (queue.size() >= 2) {
Node x = queue.poll();
Node y = queue.poll();
int freq = x.freq + y.freq;
queue.offer(new Node(freq, x, y));
}
root = queue.poll();
System.out.println(root);
//功能3:计算每个字符的编码,功能4:字符串编码后占用bits
int sum = dfs(root, new StringBuilder());
for (Node node : map.values()) {
System.out.println(node + " " + node.code);
}
System.out.println("总共会占用 bits:" + sum);
}
private int dfs(Node node, StringBuilder code) {
int sum = 0;
if (node.isLeaf()) {
//找到编码
node.code = code.toString();
sum = node.freq * code.length();
} else {
sum += dfs(node.left, code.append("0"));
sum += dfs(node.right, code.append("1"));
}
if (code.length() > 0) {
code.deleteCharAt(code.length() - 1);
}
return sum;
}
//编码
public String encode() { //000101111111
char[] chars = str.toCharArray();
StringBuilder sb = new StringBuilder();
for (char c : chars) {
sb.append(map.get(c).code);
}
return sb.toString();
}
//解码
public String decode(String str) {
char[] chars = str.toCharArray();
int i = 0;
StringBuilder sb = new StringBuilder();
Node node = root;
while (i < chars.length) {
if(!node.isLeaf()){ //非叶子
if(chars[i] == '0'){ //向左走
node = node.left;
} else if (chars[i] == '1') {//向右走
node = node.right;
}
i++;
}
if(node.isLeaf()){
sb.append(node.ch);
node = root;
}
}
return sb.toString();
}
/*
Huffman 树的构建过程
1.将统计了出现频率的字符,放入优先级队列
2.每次出队两个频次最低的元素,给它俩找个爹
3.把爹重新放入队列,重复2~3
4.当队列只剩一个元素时,Huffman树构建完成
*/
public static void main(String[] args) {
HuffmanTree tree = new HuffmanTree("abbccccccc");
String encode = tree.encode();
System.out.println(encode);
System.out.println(tree.decode(encode));
}
}