目的
- 直观的了解哈弗曼树与哈弗曼编码
- 证明: 哈弗曼编码是最优的变⻓编码
前置知识
什么是编码
ASCLL编码:
‘a’ = 97 =
0110
000
1
2
0110\ 0001_2
0110 00012
‘0’ = 48 =
0011
000
0
2
0011\ 0000_2
0011 00002
注意:计算机中任何信息都是以二进制存储的
信息‘aa00’ = 0110 0001 、 0110 0001 、 0011 0000 、 0011 0000 0110\ \ 0001、0110\ \ 0001、0011\ \ 0000、0011\ \ 0000 0110 0001、0110 0001、0011 0000、0011 0000在一台计算机传输至另一台计算机,传输 32 个比特位 假设计算机的网络是 32bit/s, 则用时 1s
特定场景:只有 a,b,0,1四种字符需要传输
自定义编码:
a:00 b:01 0: 10 1: 11
“aa00” = 00 、 00 、 10 、 10 00、00、10、10 00、00、10、10
在带宽不变的情况下,当前只需要传输 0.25s
定长编码与变长编码
- ASCLL 码与特定场景的自定义编码都属于定长编码
- 对于每一个字符,编码长度都相同,这就是定长编码
- UTF-8 是变长编码,UTF-16是定长编码
- 对于每一个字符,编码长度不相同,这就是变长编码
- 变长编码一定不差于定长编码
变长编码应用场景
特定场景:
- 只有四种字符,a b 0 1
- 字符出现的概率不同:
a : 0.8, b: 0.05, 0: 0.1, 1: 0.05
平均编码长度:
l i l_i li: 每个字符的编码长度
p i p_i pi: 每个字符出现的概率
a v g ( l ) = ∑ ( l i ∗ p i ) avg(l) = \sum(l_i * p_i) avg(l)=∑(li∗pi)
假设平均编码长度为 1.16, 则传输 100 个字符需要传输 116 个比特位
自定义平均编码长度 a v g ( l ) = 2 ∗ ∑ ( p i ) = 2 avg(l) = 2 * \sum(p_i) = 2 avg(l)=2∗∑(pi)=2
新的自定义编码:
a: 1 , b: 01, 0: 000, 1:001
平均编码长度:
a v g ( l ) = 1 ∗ 0.8 + 2 ∗ 0.05 + 3 ∗ 0.1 + 3 ∗ 0.05 = 1.35 avg(l) = 1*0.8+2*0.05+3*0.1+3*0.05 = 1.35 avg(l)=1∗0.8+2∗0.05+3∗0.1+3∗0.05=1.35
100个字符,平均只需传输 135 个比特位。
哈弗曼编码
- 首先,统计每一种字符出现的概率
- 将 n 个字符,建立为一棵哈弗曼树
- 每一个字符都落在叶子节点上
- 按照左 0 右 1 的形式,将编码读取出来
举例:
新的哈弗曼编码:
a:0, b:110, 0: 10, 1:111
因为每一个字符都落在叶子节点上,所以不可能有某个字符是另一个字符的前缀。
平均编码长度: a v g ( l ) = 1 ∗ 0.8 + 3 ∗ 0.05 + 2 ∗ 0.1 + 3 ∗ 0.05 = 1.3 avg(l) = 1*0.8+3*0.05+2*0.1+3*0.05 = 1.3 avg(l)=1∗0.8+3∗0.05+2∗0.1+3∗0.05=1.3
结论:哈弗曼编码是最优的变长编码。
证明:哈弗曼编码是最优的变长编码
证明: E = ∑ i ( p i ∗ l i ) E = \sum_i(p_i*l_i) E=∑i(pi∗li)最小。
假设哈弗曼树的树高为 H(树高从 0 开始),在第
l
l
l层的某个节点,其所覆盖的叶子节点数量为
2
(
H
−
l
)
2^{(H-l)}
2(H−l)。
第 i i i个字符的编码长度为 l i l_i li,在哈弗曼树对应的 l i l_i li层,所能覆盖的叶子节点数量为 2 H − l i 2^{H-l_i} 2H−li,约束条件为:
2 H − l 1 + 2 H − l 2 + . . . + H − l n < = 2 H 2^{H-l_1} + 2^{H-l_2 + ... + H-l_n} <= 2^H 2H−l1+2H−l2+...+H−ln<=2H
两边同时除以 2 H 2^H 2H,不等式变成:
1 2 l 1 + 1 2 l 2 + . . . + 1 2 l n < = 1 \frac{1}{2^{l_1}}+\frac{1}{2^{l_2}} + ... + \frac{1}{2^{l_n}} <= 1 2l11+2l21+...+2ln1<=1
设 l i ′ = − l i l_i' = -l_i li′=−li,则上述不等式变成
2 l 1 ′ + 2 l 2 ′ + . . . + 2 l n ′ < = 1 2^{l_1'} + 2^{l_2'} + ... + 2^{l_n'} <= 1 2l1′+2l2′+...+2ln′<=1
问题为求 l i l_i li,使得 ∑ i ( p i ∗ l i ) \sum_i(p_i * l_i) ∑i(pi∗li)最小,即求 l i ′ l_i' li′,使得 − ∑ i ( p i ∗ l i ′ ) -\sum_i(p_i * l_i') −∑i(pi∗li′)最小,约束条件为:
∑ i ( 2 l i ′ ) < = 1 \sum_i(2^{l_i'}) <= 1 i∑(2li′)<=1
设 I i = 2 l i ′ I_i=2^{l_i'} Ii=2li′,则 l i ′ = l o g 2 ( I i ) l_i' = log_2(I_i) li′=log2(Ii).则问题同构为:
目标: − ∑ i ( p i ∗ l o g 2 ( I i ) ) -\sum_i(p_i * log_2(I_i)) −∑i(pi∗log2(Ii))最小;
约束条件为: ∑ i ( I i ) < = 1 \sum_i(I_i) <= 1 ∑i(Ii)<=1。
重写目标最小值函数:
− ( p 1 l o g ( I 1 ) + p 2 l o g ( I 2 ) + . . . + I n l o g ( p n ) ) -(p_1log(I_1) + p_2log(I_2) + ... + I_nlog(p_n)) −(p1log(I1)+p2log(I2)+...+Inlog(pn))
约束条件:
I 1 + I 2 + . . . + I n < = 1 I_1 + I_2 + ... + I_n<= 1 I1+I2+...+In<=1
假设目标函数达到最小值时, I 1 + I 2 + . . . + I n < 1 I_1 + I_2 + ... + I_n< 1 I1+I2+...+In<1
设余量 1 − ∑ ( I i ) = I x ′ 1-\sum(I_i) = I_x' 1−∑(Ii)=Ix′
不妨设 I x ′ I_x' Ix′可以加到 I i I_i Ii上,但是 I i I_i Ii加一个余量时,目标函数 − ( p 1 l o g ( I 1 ) + p 2 l o g ( I 2 ) + . . . + I n l o g ( p n ) ) -(p_1log(I_1) + p_2log(I_2) + ... + I_nlog(p_n)) −(p1log(I1)+p2log(I2)+...+Inlog(pn))又会变小一点,所以余量 I x ′ I_x' Ix′一定等于0。从而得出,当目标函数达到最小值时,一定有:
I 1 + I 2 + . . . + I n = 1 I_1 + I_2 + ... + I_n= 1 I1+I2+...+In=1
即
I
n
=
1
−
(
I
1
+
.
.
.
+
I
n
−
1
)
I_n = 1-(I_1 + ... + I_{n-1})
In=1−(I1+...+In−1)
令 J = ( I 1 + . . . + I n − 1 ) J=(I_1 + ... + I_{n-1}) J=(I1+...+In−1)
则目标函数变为:
F = − ( p 1 l o g ( I 1 ) + p 2 l o g ( I 2 ) + . . . + p n l o g ( 1 − J ) ) F = -(p_1log(I_1) + p_2log(I_2) + ... + p_nlog(1-J)) F=−(p1log(I1)+p2log(I2)+...+pnlog(1−J))
令 F 对 I i I_i Ii求偏导,F 达到最小值时偏导应都为 0,从而可以得到:
p i I i ln 2 = p n ( 1 − J ) ln 2 \frac{p_i}{I_i\ln2} = \frac{p_n} {(1-J)\ln2} Iiln2pi=(1−J)ln2pn
即 p 1 I 1 = p 2 I 2 = . . . = p n − 1 I n − 1 = p n I n \frac{p_1}{I_1} = \frac{p_2}{I_2}=... = \frac{p_{n-1}}{I_{n-1}}=\frac{p_n}{I_n} I1p1=I2p2=...=In−1pn−1=Inpn
又因为 ∑ i ( p i ) = 1 \sum_i(p_i) = 1 i∑(pi)=1
∑ i ( I i ) = 1 \sum_i(I_i) = 1 i∑(Ii)=1
所以当目标函数达到最小值时一定有 p i = I i p_i = I_i pi=Ii
熵的定义为: − ∑ ( p i log ( p i ) ) -\sum(p_i\log(p_i)) −∑(pilog(pi))
正好是 p i = I i p_i = I_i pi=Ii的情况。交叉熵 − ∑ ( p i log ( q i ) ) -\sum(p_i\log(q_i)) −∑(pilog(qi))就表示概率向量 p i p_i pi和 q i q_i qi的相似程度,越相似交叉熵越小。
某个字符的概率越大,对应的编码长度应该越小,这样的情况下平均编码长度最小,这就是哈弗曼编码。
证毕。
注意这种证明并不严谨,因为证明中 l i l_i li可以是小数,求导也是实数域,但是实际过程中只能是整数。
证明过程总结:
- 首先表示平均编码长度,求解公式最优解
- 最终,推到得出和熵与交叉熵之间的关系。
哈夫曼编码的代码演示
#include <stdio.h>
#include <stdlib.h>
#define swap(a, b) {\
__typeof(a) temp = a;\
a = b;\
b = temp;\
}
typedef struct Node {
char ch;
double p;
struct Node *lchild, *rchild;
}Node;
Node *getNewNode(char ch, double per) {
Node *p = (Node *)malloc(sizeof(Node));
p->ch = ch;
p->p = per;
p->lchild = p->rchild = NULL;
return p;
}
Node *combineNode(Node *a, Node *b) {
Node *p = getNewNode(0, a->p + b->p);
p->lchild = a;
p->rchild = b;
return p;
}
void pick_min(Node **arr, int n) {
for (int j = n - 1; j >= 0; j--) {
if (arr[j]->p < arr[n]->p) {
swap(arr[j], arr[n]);
}
}
return;
}
Node *getHaffmanTree(Node **arr, int n) {
for (int i = 1; i < n; i++) {
pick_min(arr, n - i);
pick_min(arr, n - i - 1);
arr[n - i - 1] = combineNode(arr[n - i], arr[n - i - 1]);
}
return arr[0];
}
void __output_encode(Node *root, char *str, int k) {
str[k] = 0;
if (root->lchild == NULL && root->rchild == NULL) {
printf("%c %s\n", root->ch, str);
return;
}
str[k] = '0';
__output_encode(root->lchild, str, k+1);
str[k] = '1';
__output_encode(root->rchild, str, k+1);
return;
}
void output_encode(Node *root) {
char str[100];
__output_encode(root, str, 0);
return;
}
void clear(Node *p) {
if (p == NULL) return;
clear(p->lchild);
clear(p->rchild);
free(p);
return;
}
int main() {
int n;
Node **arr;
scanf("%d", &n);
arr = (Node **)malloc(sizeof(Node *) * n);
for (int i = 0; i < n; i++) {
char ch[10];
double p;
scanf("%s%lf", ch, &p);
arr[i] = getNewNode(ch[0], p);
}
Node *root = getHaffmanTree(arr, n);
output_encode(root);
clear(root);
free(arr);
return 0;
}
总结:使用c语言实现,按照哈夫曼编码的基本定义实现。