《趣学算法》学习打卡Day 2
《趣学算法》学习打卡:Day2
神秘电报密码——哈夫曼编码(哈夫曼树)
哈夫曼编码 :
(一):编码尽可能的短:根据字符使用频率不同(权值不同),当进行编码储存时,频率高的编码短,频率低的编码长(不定长编码),从而节省储存空间。
注: 如果字符使用的频率是相同的,固定长度编码时空间效率最高的办法
(二):消除二义性(前缀码特性);一个高频字符(编码短的)不能是其他低频字符的前缀。
例如: a=0,b=1,c=01,d=11 ,a是c的前缀,b是d的前缀,用来储存时就会产生二义性
哈夫曼编码的思路是以字符的使用频率作为权来构建一棵哈夫曼树,利用哈夫曼树对字符进行编码。
哈夫曼算法采取的贪心策略是每次从树的集合中取出没有双亲且权值最小的两棵树作为左右子树,构造一个新树,新树根结点的权值为其左右孩子结点的权值之和,将新树插入到树的集合中。
先来捋一捋思路再看代码吧,下面都是书上给的伪码,一下子看有点串
首先我们清楚完成哈夫曼编码是有两个步骤的:
- 构建哈夫曼树
关于数据:假设我们有n个字符,那么我们最后的到的哈夫曼树一共有多少个结点呢?答案是2*n-1个,恰好是每个字符中间插入一个结点来组成树插入的结点数(n-1);
那么怎么去表示这些结点呢?这时候就需要设计一个结点结构体了,(见下文:节点结构体)
在设计好结构体以后,就只要简单的初始化结点的数据和录入n个字符及其权值就okk了;
在数据都准备好了的现在;按照哈夫曼贪心策略,选择结点(有权值的)中权值最小的两个结点,把他们的权值之和赋给下一个权值为空的结点的权值,把他们的ID按照左小右大计入左右子树中(下标)。
重复上面的步骤到2*(n-1)中的权值不为0;
这里应该要配图的,不然太抽象了,ppt演示这种需要动态变化的一定会更直观
假设n=6={a=5,b=32,c=18,d=7,e=25,f=13};则
哈夫曼结点数组:
i weight parent lchild rchild value 0 5 -1->6 -1 -1 a 1 32 -1 -1 -1 b 2 18 -1 -1 -1 c 3 7 -1->6 -1 -1 d 4 25 -1 -1 -1 e 5 13 -1 -1 -1 f 6 0—>12 -1->7 -1->0 -1->3 7 0->25 -1 -1->6 -1->5 8 0 -1 -1 -1 9 0 -1 -1 -1 10 0 -1 -1 -1 好了,这个画图有点浪费时间,下面的自己脑补一下吧
- 输出哈夫曼编码
ok,到这里你应该知道,我们是怎样构建哈夫曼树的了,如果你还不会,那么请仔细阅读上面的部分。或者直接参考课本的相应章节。
重点在于怎么借助哈夫曼树构建编码
对于字母符的结点,他是一定有双亲的,我们从当前(字符)叶子结点出发,去找到双亲,判断自己是双亲的左子树还是右子树,左子树记为0,右子树记为1,写在编码结构体的HCodeType.bit[]中(从后面往前面记,输出的时候正着输出就是这个字符的编码);然后找双亲的双亲(祖父母??),判断……,直到结点没双亲。
- 数据结构:
结点结构体:
typedef struct
{
double weight;//权值
int parent; //双亲
int lchild; //左孩子
int rchild; //右孩子
char value; //代表的字符
}HNodeType
| weight | parent | lchild | rchild | value |
|---|---|---|---|---|
| 0 | -1 | -1 | -1 |
注:-1表示没有**
编码结构体:
typedef struct
{
int bit[MAXBIT];//储存编码的数组
int start; //编码开始的下标
}HcodeType
2.初始化
结点初始化
for(int i=0;i<2*n-1;i++)
{
HuffNode[i].weight=0;
HuffNode[i].parent=-1;
HuffNode[i].lchild=-1;
HuffNode[i].rchild=-1;
}
/*输入n个叶子结点的字符及权值*/
for(i=0;i<n;i++)
{
cout<<"please input value and weight of leaf node"<<i+1<<endl;
cin>>HuffNode[i].value>>HuffNode[i].weight;
}
输入n个叶子结点的字符及权值
3.循环构造Huffman树
所有的结点都放在一个集合T中表示
s从树集合中取出两个权值最小的树ti,tj,把他们合成一棵新树zk,新树的左儿子为ti,右孩子为tj,Zk的权值为ti和tj的权值之和。
int i,j,x1,x2;//x1、x2为最小权值结点的序号。
double m1,m2;//m1、m2为最小权值结点的权值。
for(i=0;i<n-1;i++)
{
m1=m2=MAXVALUE;
x1=x2=-1;
for(j=0;j<n+i;j++)//为什么不对字符的权值进行排序?效率那个更好?
{
if(HuffNode[j].weight<m1&&HuffNode[j].parent=-1){
m2=m1;
x2=x1;
m1=HuffNode[j].weight;
x1=j;
}
else if(HuffNode[j].weight<m2&&HuffNode[j].parent==-1){
m2=HuffNode[j].weight;
X2=j;
}
}
/*更新树的信息*/
HuffNode[x1].parent=n+i;
HuffNode[x2].parent=n+i;
HuffNode[n+i].weight=m1+m2;
HuffNOde[n+i].lchild=x1;
HuffNode[n+i].rchild=x2;
}
4.输出哈夫曼编码
void HuffmanCode(HCodeType Huffcode[MAXLEAF],int n)
{
/*定义一个临时变量来存放求解编码时的信息*/
HCodeType cd;
int i,j,c,p;
for(i=0;i<n;i++) //恰好时字符数
{
cd.start=n-1; //最后一个字符?
c=i; //i为叶子结点的编号
p=HuffNode[c].parent;//p为叶子结点双亲的编号
/*while循环:从当前结点出发,一直遍历到树根,得到编码数组bit[]*/
while(p!=-1)
{
/*如果i为双亲的左孩子***赋值0,为右孩子***赋值1*/
if(HuffNode[p].lchild==c){
cd.bit[cd.strat]=0;
}
else
cd.bit[cd.strat]=1;
/*哈夫曼编码数组向前移动,*/
cd.strat--;
/*c,p变量上移,准备下一循环*/
c=p;
p=HuffNode[c].parent;
}
/*把叶子结点的编码信息从临时编码cd中复制出来,放入编码结构体数组*/
for(j=cd.strat+1,j<n;j++)
HuffCode[i].bit[j]=cd.bit[j];
HuffCode[i].strat=cd.strat;
}
}
//毕竟这是别人写的,对数组的控制有没有打注释,想看仔细看清内部逻辑还是比较难的,也就是了解功能就ok了,不要太过于纠结细节,这一点我浪费了好多时间!
总结:
还是犯二了,不该去纠结人家的代码的,像那种一段段的代码,知道其功能以后就该跳走的,浪费时间去研究人家for()循环的数组下标控制,真的就是naocan。那些东西应该在自己写代码的时候再去考虑的,那样才会有整个代码全局数据控制的视角,那样才能迅速反应过来。
可以明显感觉到难度了,昨天是图,今天是树,明天是图&树;等明天的“沟通无限校园——最小生成树”完了以后,后天就直接把最短路径、哈夫曼、最小生成树给实现了(可能要两天)!1+1+1+2=5*20=100;刚刚好5天100页;可以用两天的时间来实现代码。
| 明天 | 后天 | 大后天 |
|---|---|---|
| 2020/8/29 | 2020/8/30 | 2020/9/1 |
| 完成最小生成树的算法思路理解 | 完成最短路径和哈夫曼的实战演练 | 完成最小生成树的代码实现 |
我还是觉得,把书中的思路捋清楚就该跳过的,代码实现也许很重要,但那真的影响阅读的积极性!

浙公网安备 33010602011771号