理论部分
先补充一个概念,什么是路径长度?
从树中一个节点到另一个节点之间的分支构成这两个节点之间的路径,路径上的分支数目称作路径长度。而一般不带权的单个路径长度默认为1,所以可以认为节点数为n的树的路径长度为n-1。
哈夫曼树的定义是带权路径长度最短的树,也叫最优二叉树。换种更好的理解方式,就是一棵特殊的二叉树,而这棵树的叶子节点到根节点的带权路径都是尽可能最短的
如下图:
树a的路径长度就是7*2+5*2+2*2+4*2=36。
树b的路径长度就是7*3+5*3+2*1+4*2=46
树c的路径长度就是7*1+5*2+2*3+4*3=35
明显,树c的路径长度最小,但它是最优二叉树吗?
想要验证树c是不是哈夫曼树,就得从定义出发。带权路径长度最小,那么很容易想到,权重大的路径所在的层数一定偏小,而权重小的路径层数就偏小。那么很容易联想到,把权重小的路径先找出来并放在下面。经过总结可得出以下结论:
(1)根据给定的n个权值 {w1,w2,⋯,wn} 构成n棵二叉树的集合 F={T1,T2,⋯,Tₙ}, 其中每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树均空。
(2)在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
(3)在F中删除这两棵树,同时将新得到的二叉树加入F中。
(4)重复(2)和(3),直到F只含一棵树为止。这棵树便是赫夫曼树。
哈夫曼编码与前缀编码:
前缀编码:如果在一个编码方案中, 任一个编码都不是其他任何编码的前缀(最左子串),如称编码是前缀编码。前缀编码可以保证对压缩文件进行解码时不产生二义性,确保正确解码。
哈夫曼编码:对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支赋予0,右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一一个二进制串,该二进制串就称为哈夫曼编码。
哈夫曼编码是前缀编码:哈夫曼编码是根到叶子路径上的编码序列,由树的特点知,若路径A是另一条路径B的最左部分,则B经过了A.则A的终点一定不是叶子,而哈夫曼编码对应路径的终点一定为叶子,因此,任一哈夫曼码都不会与任意其他哈夫曼编码的前缀部分完全重叠,因此哈夫曼编码是前缀编码。
哈夫曼编码是最优前缀编码:对于包括n个字符的数据文件,分别以它们的出现次数为权值构造哈夫曼树,则利用该树对应的哈夫曼编码对文件进行编码,能使该文件压缩后对应的二进制文件的长度最短。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
实践部分
以数据结构哈夫曼树和哈夫曼编码基础题为例
请构建夫曼树和哈夫曼编码的实现,对于输入的(n=8)个字符和对应的概率,生成其对应的哈夫曼编码。
参考输入如下:
"a","b","c","d","e","f","g","h"
0.07,0.19,0.02,0.06,0.32,0.03,0.21,0.1
参考输出如下:
a: 1010
b: 00
c: 10000
d: 1001
e: 11
f: 10001
g: 01
h: 1011
首先是哈夫曼树的定义,一般要求记录节点的权重,双亲节点,左、右子女节点。
typedef struct{
int weight;//权重
int parents, Lchild, Rchlid;
}thnode,HuffmanTree[1000];
再是哈夫曼树的构建,先对前n个(叶子)节点进行处理,记录它们的权重,其他信息记为0。再对后面构建哈夫曼树形成的过程节点(容易得知哈夫曼树的节点个数为2*n-1个,所以过程节点有n-1个)的所有信息记为0。在根据理论部分的规律,进行哈夫曼树的构建。
void Huffman_Creat(HuffmanTree ht,int w[],int n)//n为叶子结点个数
{
//先处理前n个节点
int m = 2 * n - 1;//树的总结点数
int i,s1,s2;
for (i = 1; i <= n;i++)
{
ht[i].weight = w[i];
ht[i].Lchild = 0;
ht[i].Rchlid = 0;
ht[i].parents = 0;
}
//再处理后面的过程节点
for (i = n+1; i <= m;i++)
{
ht[i].weight = 0;
ht[i].Lchild = 0;
ht[i].Rchlid = 0;
ht[i].parents = 0;
}
//最后再按规律构建哈夫曼树
for (i = n + 1; i <= m;i++)
{
select(ht, i - 1, &s1, &s2);//寻找双亲节点不为0的权重最小的2个节点
ht[i].weight = ht[s1].weight + ht[s2].weight;
ht[i].Lchild = s1;
ht[i].Rchlid = s2;
ht[s1].parents = i;
ht[s2].parents = i;
}
}
select函数就是寻找双亲节点不为0的权重最小的2个节点。
void select(HuffmanTree ht,int end,int*s1,int*s2)
{
int min1, min2;//最小和第二小
int i = 1;
while(ht[i].parents!=0 && i<=end)
i++;
min1 = ht[i].weight;
*s1 = i;
i++;
while(ht[i].parents!=0 && i<=end)
i++;
if(ht[i].weight=min1 && ht[j].weight
哈夫曼树构建完成后,就是哈夫曼编码的建立。利用定义中的parent,可以从叶子节点寻找回根节点,跟从根节点遍历到叶子节点的方法相比,不但减少代码量,还方便记录过程编码。
typedef char* HuffmanCode[1000];
void HuffmanCode_Creat(HuffmanTree ht, HuffmanCode hc,int n)
{
char *s;
int start;
int i, c, p;
s = (char *)malloc(n * sizeof(char));//创建临时数组
s[n - 1] = '\0';//反向存入s数组中,方便后续输出
for (i = 1; i <= n;i++)
{
start = n - 1;
c = i;//存储当前节点
p = ht[i].parents;//存双亲
while(p!=0)//只有根节点的双亲节点为0
{
--start;
if(ht[p].Lchild==c)
s[start] = '0';
else
s[start] = '1';
c = p;
p = ht[p].parents;//向上寻找双亲,直到为根节点
}
hc[i] = (char *)malloc((n - start) * sizeof(char));
strcpy(hc[i], &s[start]);//将每次的结果存入hc数组
}
free(s);
}
结合本题要求,可得完整代码为:
请代入个人思考,本代码故意改了部分,输出一定是错误的!!!
请代入个人思考,本代码故意改了部分,输出一定是错误的!!!
请代入个人思考,本代码故意改了部分,输出一定是错误的!!!
#include
using namespace std;
typedef struct{
char id;
double weight;//权
int parents, Lchild, Rchlid;
}thnode,HuffmanTree[1000];
void select(HuffmanTree ht,int end,int*s1,int*s2)
{
double min1, min2;
int i = 1;
while(ht[i].parents!=0 && i<=end)
i++;
min1 = ht[i].weight;
*s1 = i;
i++;
while(ht[i].parents!=0 && i<=end)
i++;
if(ht[i].weight=min1 && ht[j].weight>n;
for (int i=1;i<=n;i++)
cin>>ppp[i];
for (int i=1;i<=n;i++)
cin>>w[i];
HuffmanTree ht;
Huffman_Creat(ht, w, n);
for (int i=1;i<=n;i++)
ht[i].id=ppp[i];
HuffmanCode hc;
HuffmanCode_Creat(ht,hc, n);
for (i = 1; i <=n; i++)
{
printf("%c: %s\n", ht[i].id,hc[i]);
}
return 0;
}