霍夫曼树以及哈夫曼编码
一、什么是哈夫曼树与哈夫曼编码
- 编码是什么
答:
在ASCII 编码中
‘a’ = 97 = ( 01100001 ) 2 (01100001)_2 (01100001)2
‘0’ = 48 = ( 00110000 ) 2 (00110000)_2 (00110000)2
注意:任何信息,在计算机中,都是二进制存储的
ASCII编码规则下的信息:“aa00” = 0110 0001、01100001、00110000、00110000
信息的价值在于流通,从一台计算机 传输到 另外一台计算机,传输上面信息,需要传输32个比特位
假设:计算机的网络带宽是32bit/s, 传上述信息,用时:1s在特定场景中:假设需要传输的只有a,b,0,1四种字符需要传输
重新编排编码a:00, b:01, 0:10, 1:11
自定义规则下的信息“aa00” = 00 00 10 10
在带宽不变的情况下,当前只需要传输0.25s
在相同传输的数据量下,越好的编码方式,传输的效率越快 -> (各个直播平台相同网络下,流畅度的区别)
- 定长与变长编码
- ASCII 编码和刚刚特定场景下的编码,都属于定长编码
- 对于每一种字符,编码长度相同,这就是定长编码
- UTF-8编码,是变长编码,UTF-16,是定长编码
- 对于每一个字符,编码长度不相同,这就是变长编码
- 将定长编码,看成时变长编码的特例
- 变长编码,一定不差于定长编码
变长编码应用场景
特定场景:
- 只有四种字符:ab01
- P(a) = 0.8, P(b) = 0.05, P© = 0.1, P(1) = 0.05
平均编码长度:
l i l_i li:第i中字符,编码长度
p i p_i pi:第i中字符,出现概率
a v g ( l ) = ∑ l i × p i avg(l) = \sum{l_i}\times{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 (问题1:为什么b的编码不能时‘10’)
平均编码长度: 1 ∗ 0.8 + 2 ∗ 0.05 + 3 ∗ 0.1 + 3 ∗ 0.05 = 1.35 1 * 0.8 + 2 * 0.05 + 3 * 0.1 + 3 * 0.05 = 1.35 1∗0.8+2∗0.05+3∗0.1+3∗0.05=1.35
意味着,传输100个字符的期望值为135比特位
优秀的编码都是根据特定场景做的优化
问题1:为什么b的编码不能是10?
解码过程,遇到和字典中匹配的字符,立刻解析字符,则当b为10的时候,碰到110,会解析为aa0 b与a冲突
哈夫曼树的建立
每一次从集合中选出两个概率最低的节点,将他们合并成一棵子树,
并将新生成的节点,放回到原集合种
哈夫曼编码
1. 首先,统计得到每一种字符的概率
2. 将n个字符,建立成一棵哈夫曼树
3. 每一个字符,都落在叶子节点上
4. 按照左0,右1的形式,将编码取出来
a : 0.8 b:0.05 0 : 0.1 1 : 0.05
***因为所有字符都落在叶子节点上,所以没有任何一个字符时另一个字符的前缀***
得到新编码
a: 0
0: 10
b: 110
c : 111
哈夫曼编码,平均编码长度:$ 1 * 0.8 + 2 * 0.1 + 3 * 0.05 + 3 * 0.05 = 1.3$
结论:哈夫曼编码时最优的变长编码 (下述证明)
二、为什么哈夫曼编码是最优的变长编码(不想了解的可以直接下一步qwq)
设 P(a) : 0.25 ,P(b) : 0.25 ,P© : 0.25 ,P(d) : 0.25
则构成的哈夫曼树为:
当P(a) = P(b) = P© = P(d) 时,哈夫曼编码退化为变长编码
所以,定长编码做的好,哈夫曼编码一定做的好,甚至做的更好
证明
证明那个指标最优? 平均编码长度最优, ∑ p i × l i \sum{p_i} \times {l_i} ∑pi×li最小
当所有字符在叶子节点上
若仅需4个编码,则需在当前的哈夫曼树中选4个节点
越往上的节点,覆盖节点越多,
越往下的节点,覆盖节点越少。
对于某个节点覆盖子节点的数量为 2 H − l 2^{H - l} 2H−l
手写推导
熵: − ∑ p i log p i -\sum{p_i} \log{p_i} −∑pilogpi 在系统种,最小的平均信息,表示单元的长度(平均编码长度)
熵越大,系统越复杂
引出
交叉熵 − ∑ p i log q i -\sum{p_i} \log{q_i} −∑pilogqi
− ∑ p i log q i -\sum{p_i} \log{q_i} −∑pilogqi 越小, p i , q i p_i , q_i pi,qi越相似 , p i = q i p_i = q_i pi=qi时,最小
− ∑ p i log q i -\sum{p_i} \log{q_i} −∑pilogqi 越大, p i , q i p_i, q_i pi,qi相差的越多
对照推导中
概率越高( p i p_i pi) L i L_i Li 越大,
l i ‘ = − l i = log L i l_i` = - l_i = \log L_i li‘=−li=logLi − l i -l_i −li 越大 l i l_i li越小 则,概率越大,路径长度越小
推到过程
1.首先表示平均每编码长度,秋节公式最优解
2.最终,和熵与交叉熵的关系
tips:
这个证明也不是很严谨的,因为哈夫曼编码是离散的,这个证明是连续的
三、霍夫曼树的代码演示
#include <stdio.h>
#include <stdlib.h>
#define swap(a, b) {\
__typeof(a) __c = a;\
a = b,b = __c;\
}
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 *CombinNode (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[n]->p > arr[j]->p) {
swap(arr[n], arr[j]);
}
}
return ;
}
Node *getHaffmanTree(Node **arr, int n) {
for (int i = 1; i < n; i++) {
//n个字符,需要合并n - 1次
pick_min(arr, n - i);//最小的放在n - i处
pick_min(arr, n - i - 1);//第2小的放在n - i - 1处
arr[n - i - 1] = CombinNode(arr[n - i], arr[n - i - 1]);
}//也可以用优先队列简化代码
return arr[0];//最后剩下的头节点,就是haffman树的根节点
}
void __output_encode(Node *root, char *str, int k) {
//k为层数
str[k] = 0;
if (root->lchild == NULL && root->rchild == NULL) {
//遇到根节点输出
printf("%c %s\n", root->ch, str);
return ;
}
str[k] = '0';//左子树为0
__output_encode(root->lchild, str, k + 1);
str[k] = '1';//右子树为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 *root) {
if (root == NULL) return ;
clear(root->lchild);
clear(root->rchild);
free(root);
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;
}
代码运行结果图