贪心算法 - 哈夫曼编码 Huffman

哈夫曼编码:

一种字符编码方式,常用于数据文件压缩。压缩率通常在20%~90%。

主要思想:

采取可变长编码方式,对文件中出现次数多的字符采取比较短的编码,对于出现次数少的字符采取比较长的编码,可以有效地减小总的编码长度。

例如,在英文中,e的出现频率最高,z的出现频率最低,所以可以用最短的编码来表示e,用最长的编码表示z。

例子:

一个文件包含100 000个字符,且仅含有a,b,c,d,e,f六个字符,那么可以用3位bit来编码这六个字符,称之为定长码。

那么采取定长码共需要的位数为 3* 100 000 = 600 000位。


这六个字符出现的频率如图所示,a最多,f最少。因此如果采取图中所示的变长码,所需要的位数为:

(45*1 + 13*3 + 12 * 3 + 16*3 + 9*4 + 5*4) * 1000 = 224 000。

可以看出压缩了25%,a的编码长度最短,为一位,f的编码长度最长,为4位。

前缀码:

当读取文件时,对于定长码文件,已经知道了每个字符的编码长度,所以只需按部就班的一个一个的读取字符即可;

对于变长码文件,各个字符的编码长度不一,因此需要小心设计各个字符的编码,一面混淆。

比如,如果a的编码为0,b的编码为01,c的编码为1,那么解码的时候如果遇到‘001’,则既可以解码成‘aac’,也可以解码成‘ab’。

因为b的编码中包含了a的编码,也就是a的编码是b的编码的前缀。

所以,变长码编码的设计,每个字符的编码都不能是其他字符编码的前缀,这种方式称之为前缀码

可以用二叉树来表示每个字符的编码,以左为0,以右为1,这样每个叶子节点的路径均不同,也都不会称为其他叶子节点的前缀。

这样每个叶子节点的路径,就是每个字符的编码。


图中形成的编码与表格中的有些差异,但是各个字符编码的长度都相同,这是左子树和右子树的区别,但是对编码长度没有影响。


代码实现:

/**
 * Huffman code
 * @author xuefeng
 *
 */
public class Huffman {

	/**
	 * ignore exception
	 * @param cs : characters
	 * @param freqs : frequency of each character
	 */
	public static void huffman(char[] cs, double[] freqs) {
		int n = cs.length;
		MinHeap<Code> heap = new MinHeap<Code>(cs.length);
		Code[] codes = new Code[n];
		for (int i = 0; i < n; i++) {
			Code c = new Code(cs[i], freqs[i]);
			heap.add(c); // 以最小堆来每次取出频率最小的两个
			codes[i] = c; // 保存所有的叶子节点
		}

		Code c, c1, c2;
		while (heap.size() > 1) {
			c1 = heap.removeMin();
			c2 = heap.removeMin();// 取出两个最小的

			c = new Code('\0', c1.freq + c2.freq);
			c.left = c1;
			c.right = c2;
			c1.parent = c;
			c2.parent = c;
			heap.add(c); // 组合成一个新的节点,并放入堆中,继续比较

			System.out.println(c1.val + "+" + c2.val + " : " + c1.freq + "+" + c2.freq + " = " + c.freq);
		}
		c = heap.removeMin(); // 二叉树根节点

		StringBuffer sb;
		for (int i = 0; i < n; i++) {
			c = codes[i]; // 从每个叶子节点,向上追溯,直到根节点,确定每个字符的编码
			sb = new StringBuffer();
			while (c != null) {
				if (c.parent != null) {
					if (c == c.parent.left) {
						sb.insert(0, 0); // 如果是左边的,编码取1
					} else {
						sb.insert(0, 1); // 如果是右边的,编码取1
					}
				}
				c = c.parent;
			}
			System.out.println(codes[i].val + " : " + sb.toString());
		}
	}

	public static void main(String[] args) {
		char[] cs = { 'a', 'b', 'c', 'd', 'e', 'f' };
		double[] freqs = { 45, 13, 12, 16, 9, 5 };// %

		huffman(cs, freqs);

		// http://zh.wikipedia.org/wiki/%E5%AD%97%E6%AF%8D%E9%A2%91%E7%8E%87
		char[] cs2 = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
				'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
				'x', 'y', 'z' };
		double[] freqs2 = { 8.167, 1.492, 2.782, 4.253, 12.702, 2.228, 2.015,
				6.094, 6.966, 0.153, 0.772, 4.025, 2.406, 6.749, 7.507, 1.929,
				0.095, 5.987, 6.327, 9.056, 2.758, 0.978, 2.360, 0.150, 1.974,
				0.074 };

		huffman(cs2, freqs2);
	}
}

class Code implements Comparable<Code> {

	public char val;

	public double freq;

	public Code left, right, parent;

	public Code(char val, double freq) {
		this.val = val;
		this.freq = freq;
	}

	@Override
	public int compareTo(Code c) {
		double d = freq - c.freq;
		return d > 0 ? 1 : (d == 0 ? 0 : -1);
	}
}

输出为:

f+e : 5.0+9.0 = 14.0
c+b : 12.0+13.0 = 25.0
#+d : 14.0+16.0 = 30.0
#+# : 25.0+30.0 = 55.0
a+# : 45.0+55.0 = 100.0
a : 0
b : 101
c : 100
d : 111
e : 1101
f : 1100
z+q : 0.074+0.095 = 0.16899999999999998
x+j : 0.15+0.153 = 0.303
#+# : 0.16899999999999998+0.303 = 0.472
#+k : 0.472+0.772 = 1.244
v+# : 0.978+1.244 = 2.222
b+p : 1.492+1.929 = 3.4210000000000003
y+g : 1.974+2.015 = 3.989
#+f : 2.222+2.228 = 4.45
w+m : 2.36+2.406 = 4.766
u+c : 2.758+2.782 = 5.54
#+# : 3.4210000000000003+3.989 = 7.41
l+d : 4.025+4.253 = 8.278
#+# : 4.45+4.766 = 9.216000000000001
#+r : 5.54+5.987 = 11.527000000000001
h+s : 6.094+6.327 = 12.421
n+i : 6.749+6.966 = 13.715
#+o : 7.41+7.507 = 14.917
a+# : 8.167+8.278 = 16.445
t+# : 9.056+9.216000000000001 = 18.272
#+# : 11.527000000000001+12.421 = 23.948
e+# : 12.702+13.715 = 26.417
#+# : 14.917+16.445 = 31.362000000000002
#+# : 18.272+23.948 = 42.22
#+# : 26.417+31.362000000000002 = 57.779
#+# : 42.22+57.779 = 99.999
a : 1110
b : 110000
c : 01001
d : 11111
e : 100
f : 00101
g : 110011
h : 0110
i : 1011
j : 001001011
k : 0010011
l : 11110
m : 00111
n : 1010
o : 1101
p : 110001
q : 001001001
r : 0101
s : 0111
t : 000
u : 01000
v : 001000
w : 00110
x : 001001010
y : 110010
z : 001001000

#代表不是叶子节点。

关于MinHeap的代码实现,请参考前一篇文章:贪心算法 - 最小生成树 Kruskal算法




  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
哈夫曼编码是一种可变长度编码,用于无损数据压缩。贪心算法可以用来求解哈夫曼编码。具体步骤如下: 1. 统计每个字符出现的频率,并将它们存储在一个数组中。 2. 创建一个最小堆,并将所有的字符节点插入其中。每个节点包含一个字符和它的频率。 3. 从最小堆中取出两个频率最小的节点,合并它们,并将新节点插入回最小堆中。新节点的频率为两个旧节点的频率之和。 4. 重复步骤3,直到最小堆中只剩下一个节点为止。这个节点就是哈夫曼树的根节点。 5. 遍历哈夫曼树,给左子树编码为0,给右子树编码为1。从根节点开始,每当向左走一步就在编码末尾添加一个0,每当向右走一步就在编码末尾添加一个1。最终得到每个字符的哈夫曼编码。 以下是C++代码实现: ```c++ #include <iostream> #include <queue> #include <vector> using namespace std; struct Node { char ch; int freq; Node* left; Node* right; Node(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {} }; struct Compare { bool operator()(Node* a, Node* b) { return a->freq > b->freq; } }; void encode(Node* root, string code, vector<string>& codes) { if (!root) return; if (root->ch != '#') codes[root->ch] = code; encode(root->left, code + "0", codes); encode(root->right, code + "1", codes); } vector<string> huffman(vector<int>& freqs) { priority_queue<Node*, vector<Node*>, Compare> pq; for (int i = 0; i < freqs.size(); i++) { if (freqs[i] > 0) { pq.push(new Node(i, freqs[i])); } } while (pq.size() > 1) { Node* left = pq.top(); pq.pop(); Node* right = pq.top(); pq.pop(); Node* parent = new Node('#', left->freq + right->freq); parent->left = left; parent->right = right; pq.push(parent); } vector<string> codes(256); encode(pq.top(), "", codes); return codes; } int main() { string s = "hello world"; vector<int> freqs(256, 0); for (char c : s) freqs[c]++; vector<string> codes = huffman(freqs); for (int i = 0; i < 256; i++) { if (freqs[i] > 0) { cout << (char)i << ": " << codes[i] << endl; } } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值