哈夫曼树与哈夫曼编码

目的

  1. 直观的了解哈弗曼树与哈弗曼编码
  2. 证明: 哈弗曼编码是最优的变⻓编码

前置知识

什么是编码

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  00010110  00010011  00000011  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 00001010

在带宽不变的情况下,当前只需要传输 0.25s

定长编码与变长编码

  1. ASCLL 码与特定场景的自定义编码都属于定长编码
  2. 对于每一个字符,编码长度都相同,这就是定长编码
  3. UTF-8 是变长编码,UTF-16是定长编码
  4. 对于每一个字符,编码长度不相同,这就是变长编码
  5. 变长编码一定不差于定长编码

变长编码应用场景

特定场景:

  1. 只有四种字符,a b 0 1
  2. 字符出现的概率不同:

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)=(lipi)

假设平均编码长度为 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)=10.8+20.05+30.1+30.05=1.35

100个字符,平均只需传输 135 个比特位。

哈弗曼编码

  1. 首先,统计每一种字符出现的概率
  2. 将 n 个字符,建立为一棵哈弗曼树
  3. 每一个字符都落在叶子节点上
  4. 按照左 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)=10.8+30.05+20.1+30.05=1.3

结论:哈弗曼编码是最优的变长编码。

证明:哈弗曼编码是最优的变长编码

证明: E = ∑ i ( p i ∗ l i ) E = \sum_i(p_i*l_i) E=i(pili)最小。

假设哈弗曼树的树高为 H(树高从 0 开始),在第 l l l层的某个节点,其所覆盖的叶子节点数量为 2 ( H − l ) 2^{(H-l)} 2(Hl)
在这里插入图片描述

i i i个字符的编码长度为 l i l_i li,在哈弗曼树对应的 l i l_i li层,所能覆盖的叶子节点数量为 2 H − l i 2^{H-l_i} 2Hli,约束条件为:

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 2Hl1+2Hl2+...+Hln<=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(pili)最小,即求 l i ′ l_i' li,使得 − ∑ i ( p i ∗ l i ′ ) -\sum_i(p_i * l_i') i(pili)最小,约束条件为:

∑ 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(pilog2(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+...+In1)

J = ( I 1 + . . . + I n − 1 ) J=(I_1 + ... + I_{n-1}) J=(I1+...+In1)

则目标函数变为:

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(1J))

令 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=(1J)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=...=In1pn1=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可以是小数,求导也是实数域,但是实际过程中只能是整数。

证明过程总结:

  1. 首先表示平均编码长度,求解公式最优解
  2. 最终,推到得出和熵与交叉熵之间的关系。

哈夫曼编码的代码演示

#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语言实现,按照哈夫曼编码的基本定义实现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值