- 疑问:它到底是干嘛的、为什么要代码要那么做?
解惑:哈夫曼树是一种特殊存储结构的树、是一种为解决实际某类问题而产生的树。 - 疑问:为什么刚开始时只是提到叶子结点,叶子结点总要有父节点的,它们在哪呢?
解惑:在解决实际问题时,我们将用到的信息看做是一个叶子结点。所以初始时,我们有N个叶子结点,然后将其构造成哈夫曼树,这就是我们需要做的工作。
- (第二天,学习了《离散数学》中树的这一节,也讲了哈夫曼树,对上面的疑问有了更好的理解。参考书上的知识点。)
- N: 叶子节点的数目
M: 最终构造出的哈夫曼树中的总的结点数
- M = 2 * N - 1:每个叶子结点都有了一个父结点,不过实际上增加的父结点个数为N - 1。看图易知:最底层是两个叶子结点共用一个父结点。
- void select(HuffmanTree ht, int i, int *s1, int *s2):
在ht[1]~ht[i]中parent值为0的结点里寻找weight最小的两个出来,将其下标保存到s1、s2中。
值得注意的是这里实际传入的参数i是变化着的,因为当我们每次寻找出一组最小权值(两个)出来时,就要改变其标记(体现在main()函数在找到s1、s2后对其parent的操作),以便下次不再考虑它们;同时以后的比较应在考虑上上面两个最小值组合出来的那个权值。这些都体现在main()里使用select()时的代码里。 - 哈弗曼编码
哈弗曼编码是一种变长编码(请看下面的“哈夫曼树的在编码中的应用”即可理解为何为变长编码),因此,使用指针数组存放每个编码串的头指针:
typedef char * HuffmanCode[N + 1]; - 哈弗曼编码的代码实现过程中遇到的问题
1)对于malloc()函数发现自己理解的还是不到位:
char *cd;
cd = (char *)malloc(n * sizeof(char));
//cd = new int[n];
/*
1、malloc出来的是一个地址/空间,空间的大小为n * sizeof(char);
2、系统知道这其实就是一个含有n个char字符的数组,因此cd指向的其实就是数组的首地址。(malloc的返回值是一个指针,指向一段可用内存的起始地址)
3、void* malloc (size_t size);函数的作用是为指针分配sizeof数量的空间;
4、函数原型的返回类型写的是void* ,所以我们在使用时会将其强转为我们需要的类型的指针。
5、new也是为其分配空间,但是new函数可以自己计算空间/内存大小的,我们只需要将数量传入。
*/
哈夫曼树在编码中的应用
在电文传输中,需要将电文中出现的每个字符进行二进制编码。在设计编码时需要遵守两个原则:
(1)发送方传输的二进制编码,到接收方解码后必须具有唯一性,即解码结果与发送方发送的电文完全一样;
(2)发送的二进制编码尽可能地短。
下面我们介绍两种编码的方式。
- 等长编码
这种编码方式的特点是每个字符的编码长度相同(编码长度就是每个编码所含的二进制位数)。假设字符集只含有4个字符A,B,C,D,用二进制两位表示的编码分别为00,01,10,11。若现在有一段电文为:ABACCDA,则应发送二进制序列:00010010101100,总长度为14位。当接收方接收到这段电文后,将按两位一段进行译码。这种编码的特点是译码简单且具有唯一性,但编码长度并不是最短的。 - 不等长编码
在传送电文时,为了使其二进制位数尽可能地少,可以将每个字符的编码设计为不等长的,使用频度较高的字符分配一个相对比较短的编码,使用频度较低的字符分配一个比较长的编码。例如,可以为A,B,C,D四个字符分别分配0,00,1,01,并可将上述电文用二进制序列:000011010发送,其长度只有9个二进制位,但随之带来了一个问题,接收方接到这段电文后无法进行译码,因为无法断定前面4个0是4个A,1个B、2个A,还是2个B,即译码不唯一,因此这种编码方法不可使用。
因此,为了设计长短不等的编码,以便减少电文的总长,还必须考虑编码的唯一性,即在建立不等长编码时必须使任何一个字符的编码都不是另一个字符的前缀,这宗编码称为前缀编码(prefix code)。
(1)利用字符集中每个字符的使用频率作为权值构造一个哈夫曼树;
(2)从根结点开始,为到每个叶子结点路径上的左分支赋予0,右分支赋予1,并从根到叶子方向形成该叶子结点的编码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 20 //叶子结点的数目
#define M (2 * N - 1) //成功构造出的哈夫曼树的结点总数
typedef char * HuffmanCode[N + 1]; //HuffmanCode创建出来的东西是个char型的指针数组
char hcc[N + 1][N + 1];
typedef struct
{
int weight;
int parent;
int LChild;
int RChild;
}HTNode, HuffmanTree[M + 1]; //由于数组中的第一个空间不存放任何东西的(即从索引值1处开始进行操作),故若想存放M个值,应申请M+1个空间,也即2 * N。
用来测试的Print函数:
void PrintHuffmanCode(HuffmanCode hc, int n)
{
int i;
for (i = 1; i <= n; i++)
{
printf("第%d个叶子结点的哈夫曼编码为:%s\n", i, hc[i]);
}
}
void PrintHuffmanTree(HuffmanTree ht, int n)
{
int i;
for(i = 1; i <= n; i++)
{
printf("ht[%d].weight:%d\t\tht[%d].parent:%d\n", i, ht[i].weight, i, ht[i].parent);
}
}
创建哈夫曼树:
void select(HuffmanTree ht, int i, int *s1, int *s2)
{
int j, k;
int weight1, weight2;
for (k = 1; k <= i; k++)
{
if (ht[k].parent == 0)
{
weight1 = ht[k].weight;
break;
}
}
//求最小权重(weight1)的叶子结点下标,由s1接收该下标
for (j = 1; j <= i; j++)
{
if (ht[j].parent == 0)
{
if (ht[j].weight <= weight1)
{
weight1 = ht[j].weight;
*s1 = j;
}
}
}
/*
开始时用来测试的语句:
printf("s1:%d\n", *s1);
printf("s1's weight:%d\n", ht[*s1].weight);
*/
//求第二小权重(weight2)的叶子结点下标,由s2接收该下标
for (k = 1; k <= i; k++)
{
if (ht[k].parent == 0 && k != *s1)
{
weight2 = ht[k].weight;
break;
}
}
for (j = 1; j <= i; j++)
{
if (ht[j].parent == 0 && j != *s1)
{
if (ht[j].weight <= weight2)
{
weight2 = ht[j].weight;
*s2 = j;
}
}
}
/*
开始时用来测试的语句:
printf("s2:%d\n", *s2);
printf("s2's weight:%d\n", ht[*s2].weight);
*/
}
void CrtHuffmanTree(HuffmanTree ht, int w[], int n)
{
int i;
for (i = 1; i <= n; i++)
{
ht[i].weight = w[i];
ht[i].parent = 0;
ht[i].LChild = 0;
ht[i].RChild = 0;
}
int m = 2 * n - 1;
for (i = n + 1; i <= m; i++)
{
ht[i].weight = 0;
ht[i].parent = 0;
ht[i].LChild = 0;
ht[i].RChild = 0;
}
printf("初始化成功!结果如下:\n");
PrintHuffmanTree(ht, n);
int s1, s2;
for (i = n + 1; i <= m; i++)//i++后得到i为m时,循环结束。因为此时若再进行新的一轮则i = m + 1 > m不符合条件,所以退出循环。最后一个结点的parent为0.
{
select(ht, i - 1, &s1, &s2);
ht[s1].parent = i;
ht[s2].parent = i;
ht[i].LChild = s1;
ht[i].RChild = s2;
ht[i].weight = ht[s1].weight + ht[s2].weight;
/*
开始时用来测试的语句:
printf("ht[%d].weight:%d\n", i, ht[i].weight);
*/
}
}
得到哈夫曼代码:
void CrtHuffmanCode(HuffmanTree ht, HuffmanCode hc, int n)
//从叶子结点到根(注意:并不代表for循环要从尾开始!),逆向求每个叶子结点对应的哈夫曼编码。
{
char *cd;
cd = (char *)malloc(n * sizeof(char)); //分配一个数组
cd[n - 1] = '\0'; //数组的最后一个存放结束符
int start, c, p;
int i;
for (i = 1; i <= n; i++)
{
start = n - 1;
c = i; //c表示当前的叶子结点
p = ht[i].parent; //p表示当前叶子结点的父结点。因为你要知道叶子结点是其父结点的左孩子还是右孩子,所以需要p保存下来它的父节点!
while (p != 0) //创建编码的过程:要一直向上走,直至走到最上面
{
--start; //第一次执行时:跳过最后一个,因为已经保存了结束符
//其他时候执行时:当前的cd[start]已经保存了一个码,所以再使用时,要先--
if (ht[p].LChild == c) //被判断的这个叶子结点如果是父节点的左孩子,那么对应编码为0
cd[start] = '0';
else
cd[start] = '1'; //被判断的这个叶子结点如果是父节点的右孩子,那么对应编码为1
//c、p的不断变化才使得一直向上找!
c = p;
p = ht[p].parent;
}
//while循环结束,表示已计算出一个叶子结点的哈弗曼编码。下面将cd[]数组保存的这个哈夫曼编码复制到hc中。
hc[i] = (char *)malloc((n - start) * sizeof(char));
//实际编码个数为n - start - 1(这里n - start是算上了结束符所占的空间)
hc[i] = hcc[i];
strcpy(hc[i], &cd[start]); //从cd[start]所在的地址开始,进行字符串的复制,因为start前面的我们没有对其初始化。这对应我们上面的步骤——求哈夫曼编码的过程是倒着求的。
}
free(cd);
}
main()函数:
int main()
{
HuffmanTree ht;
HuffmanCode hc;
int n, w[N + 1] = { 0 }; //下标从1开始操作,因此w的下标设置为(N+1)。
printf("请输入叶子节点的数量n(n∈(0, 20]): ");
scanf("%d", &n);
printf("请依次输入这些结点的权重:");
int i;
for (i = 1; i <= n; i++)
{
scanf("%d", &w[i]);
}
CrtHuffmanTree(ht, w, n);
printf("成功创建哈夫曼树!结果如下:\n");
PrintHuffmanTree(ht, 2*n-1);
CrtHuffmanCode(ht, hc, n);
printf("成功得到哈夫编码!结果如下:\n");
PrintHuffmanCode(hc, n);
return 0;
}
运行示例: