今天的图有点丑,见谅(✿◡‿◡)
文章目录
目录
信息熵
平均编码长度:设传输一组数据a,b,c,d即我们要对其进行二进制的编码,长度分别是La,Lb,Lc,出现的概率分别是Pa,Pb,Pc,Pd。
平均编码长度就是L=La*Pa+Lb*Pb+Lc*Pc+Ld*Pd……(k=1,n)∑LkPk.累加格式没有找到,先浅浅的这样表示。
因为要对信息进行二进制编码,为了便于理解,我们用一个二叉树来讲解,往左走是1,往右走是0。
比如c结点的编码就是10.
我们所研究的是变长编码系统,每一种字符编码长度不一样,观察这个二叉树可知,任何两个结点不可能出现在同一条路径上,即所覆盖的叶子结点不可能重合。
例:比如下图中的m,b出现在了同一条路径上,如果传过来一串编码110,那么对于变长编码系统而言我们无法判断这个编码是m(11)和d(0)还是b(110).
那么我们在选择编码结点的时候保证它们所覆盖的叶子节点没有交集就可以。
看d结点,它的编码长度是1,所覆盖的叶子结点的个数是2²,设树的高度为h,编码长度为l,它所覆盖的叶子结点个数是2^(h-l).
那我们此时把得出的这个结论带入到L=(k=1,n)∑LkPk中,此时累加和刚好等于2^h.
把所有编码结点所能覆盖的叶子结点相加小于等于2^h,不等式两边同时除以2^h,右式等于一,左式可看作概率,此时做一步替换Lk_=-Lk,此时我们的目标函数
L=-(Pa*La_+Pb*Lb_+Pc*Lc_+Pd*Ld_)
令Ik=2^(Lk_),就可得L=-(Pa*log2(Ia)+Pb*log2(Ib)+Pc*log2(Ic)+Pd*log2(Id)……+Pn*log2(In))。
易得:当I的累加和等于1的时候解最优。
证:假设I的累加和此时小于1,就会有一个余项即1-I的累加和,将这个余项加到任意一项中都会使递增函数log2(Ik)变大加上前面的负号即L就会变小,此时具有更优解,那么易知当I的累加和等于1的时候解最优。
令II=(k=1,n-1)∑I.
L=-(Pa*log2(Ia)+Pb*log2(Ib)+Pc*log2(Ic)+Pd*log2(Id)……Pn-1*log2(I(n-1))+Pn*log2(I(1-II))。
我们再对每个Ik求偏导令其等于零求最小:
总结就是P1/I1=P2/I2=.....Pn/In
由于所有P的累加和等于1,所有I的累加和等于1,可得Pk=Ik。
最后推出最小平均编码长度公式为
L=-(Pa*log2(Pa)+Pb*log2(Pb)+Pc*log2(Pc)+Pd*log2(Pd))……Pn*log2(Pn))
我们进行了两部变换:1)Lk_=-Lk
L=-(Pa*La_+Pb*Lb_+Pc*Lc_+Pd*Ld_)
2)log2(Ik)=Lk_
L=-(Pa*log2(Ia)+Pb*log2(Ib)+Pc*log2(Ic)+Pd*log2(Id))
哈夫曼编码
哈夫曼编码的作用就是使平均编码长度达到最小值。
假设选择如上编码结点,那么说明此时我们树高为H-1,即所有字符编码不落在叶结点上。所以哈夫曼编码中一定有一些编码结点落在叶子结点上。
如果把代表这些编码结点单独画出来,那么,这些编码结点就是画出的这棵树上的所有叶子结点,如图:
在这个树中,我们很容易得知2,3结点的概率最低,因为编码长度最长,而此时p5等于p3+p2。结点五相当于2或3.
那么我们就可以选择出现概率最低的结点进行合并。即最先被合并的一定是概率最小的,树的路径长度最长。即这就是我们的哈夫曼树。哈夫曼编码就是编码结点。
哈夫曼编码代码演示
#include<bits/stdc++.h>
#include<stdlib.h>
#include<string.h>
using namespace std;
typedef long long ll;
#define swap(a,b){\
__typeof(a) temp=a;\
a=b;b=temp;\
}
typedef struct Node{//结点结构
char ch;
double p;//当前结点的概率值
struct Node*next[2];//0 1
}Node;
typedef struct Code{
char ch;
char *str;
}Code;//
//树形结构
typedef struct HaffmanTree{
Node *root;//指向根节点的一个指针
int n;
Code*codes;
}HaffmanTree;
typedef struct Date{
char ch;
double p;
}Date;
/*class Date{
public :
char ch;
double p;
}Date;*/
Date arr[1000];
Node*getNewNode(Date*obj){
Node *p=(Node*)malloc(sizeof(Node));
p->ch=(obj?obj->ch:0);
p->p=(obj?obj->p:0);//判断obj是否为空
p->next[0]=p->next[1]=NULL;
return p;
}
HaffmanTree*getNewTree(int n){//维护n个字符
HaffmanTree*tree=(HaffmanTree*)malloc(sizeof(HaffmanTree));
tree->codes=(Code*)malloc(sizeof(Code)*n);
tree->root=NULL;
tree->n=n;
return tree;
}
void insert(Node**arr,int n){
for(int j=n;j>=1;j--){
if(arr[j]->p<arr[j-1]->p){
swap(arr[j],arr[j-1]);
}
else break;
}
return ;
}
int extractCodes(Node*root,Code*a,int k,int l,char *buff){
//当前结点是叶子结点时,能表示一个编码
buff[l]=0;
if(root->next[0]==NULL){//哈夫曼中没有度为零的结点
a[k].ch=root->ch;
a[k].str=strdup(buff);
return 1;//编码了一个字符
}
//不是一个叶子结点
int dd=0;//表示编码了多少个字符
buff[l]='0';//向左子树走
dd+=extractCodes(root->next[0],a,k+dd,l+1,buff);
buff[l]='1';//向right子树走
dd+=extractCodes(root->next[1],a,k+dd,l+1,buff);
return dd;
}
HaffmanTree*build(Date*arr,int n){
Node**nodes=(Node**)malloc(sizeof(Node)*n);//存储哈夫曼结点地址的数组
for(int i=0;i<n;i++){
nodes[i]=getNewNode(arr+i);
}
//进行n-1轮的合并,先排序概率
for(int i=0;i<n;i++){//插入排序
/*for(int j=i;j>=1;j--){
if(arr[j]->p>arr[j-1]->p)//(((((((
{
swap(arr[j],arr[j-1]);
}
break;
}*/
insert(nodes,i);
}
/*因为最小的两个在数组的最后两位,
我们要将最小的两位合并并把合并完的数据存储在下一轮合并的最后一位
然后将这个结点进行向前插入排序 */
for(int i=n-1;i>=1;i--){//最后一轮合并,剩两个元素
Node*p=getNewNode(NULL);
//p就是新生成的结点
p->next[0]=nodes[i];
p->next[1]=nodes[i-1];
p->p=nodes[i]->p + nodes[i-1]->p;
//合并完成,此时要将这个结点放到i-1处
nodes[i-1]=p;
/*for(int j=i-1;j>=1;j--){
if(arr[j]->p>arr[j-1]->p){
swap(arr[j],arr[j-1]);
}
break;
}*/
insert(nodes,i-1);
}
char *buff=(char*)malloc(sizeof(char)*n);
HaffmanTree*tree=getNewTree(n);
tree->root=nodes[0];
free(nodes);
extractCodes(tree->root,tree->codes,0,0,buff);
free(buff);
return tree;
}
int main()
{
char str[10];
int n;
cin>>n;
for(int i=0;i<n;i++){
scanf("%s%d",str,&arr[i].p);
//用字符串的第一位,防止换行和空格的吞入很久之前学到的小技巧 比较好用
arr[i].ch=str[0];
}
HaffmanTree*tree=build(arr,n);
for(int i=0;i<tree->n;i++){
cout<<tree->codes[i].ch<<":"<<tree->codes[i].str<<endl;
}
return 0;
}
能让人疯狂的事情果然只有找bug。