哈夫曼(Huffman)树 & 哈夫曼编码与解码

哈夫曼(Huffman)树 & 哈夫曼编码与解码

目录

哈夫曼树

什么是哈夫曼树

怎么构建哈夫曼树

哈夫曼编码

编码演示

前缀码

代码实现

代码思路

数据结构

队列相关操作(代码实现)

队列的初始化

入队

出队

构建Huffman树(代码实现)

测试队列

测试Huffman树

为Huffman树进行编码(代码实现)

建表

编码逻辑

测试建表

编码

解码

完整代码

哈夫曼树

什么是哈夫曼树

哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。

带权路径长度:WPL(Weighted Path Length)是树中所有叶子结点的带权路径长度之和。

举例:

公式:WPL=(W1*L1+W2*L2+W3*L3+...+Wn*Ln)

这里:WPL = 10 * 1 + 18 * 2 + (10 + 20) * 3 = 136

怎么构建哈夫曼树

哈夫曼树是一种带权路径长度最短的二叉树。怎么构建哈夫曼树,也即怎么构建带权路径最短的树?

举例:一组字符 A、B、C、D、E 出现的频率分别是:{12, 9, 13, 6, 3}。

将五个字符看作叶子节点,频率作为节点的权重。

构建哈夫曼树的过程如下:

(1)从{12, 9, 13, 6, 3}中取出频率最小的两个:6、3,对应字符作为子节点,两节点权重之和作为父节点:

(2)将生成的新节点的权重9并入集合,也即{12,9, 13,9}。对应的节点和权重:

(3)从{12,9, 13,9}取出权重最小的两个:9、9,对应字符作为子节点,两节点权重之和作为父节点:

(4)将生成的新节点的权重18并入集合,也即{12,13,18}。对应的节点和权重:

(5)从{12,13,18}取出权重最小的两个:12、13,对应字符作为子节点,两节点权重之和作为父节点:

(6)将生成的新节点的权重25并入集合,也即{18,25}。对应的节点和权重:

(7)从{18。25}取出权重最小的两个:18、25,对应字符作为子节点,两节点权重之和作为父节点:

(8)哈夫曼树构建完毕。

总结:每次从权重集合取出权重最小的两个节点,生成一个中间节点作为这两节点的父节点,再将父节点并入集合,直到完整的树构建完毕。

哈夫曼编码

编码演示

我们已经知道了哈夫曼树的构建过程,怎么进行编码呢?

哈夫曼树中,除叶子节点外,每个节点都有左右孩子。那么我们直接给左编码0,右编码1。如图:

仅需要给叶子节点编码。

相对于根节点,A是根节点的左孩子的左孩子(或者看作:根节点到达A经过 0、0),故A的编码就是:00

同理,相对于根节点,B是根节点的右孩子的左孩子(或者看作:根节点到达B经过 1、0),故B的编码就是:10

所以对这颗哈夫曼树进行完整编码就是:A:00,B:10,C:01,D:110,E:111

这就得出了一种Huffman编码表:

ABCDE001001110111

试试能不能给出一段码文然后进行解码:

11001011000111110

有没有发现,我们既可以将这段码文代入编码表中进行解码,也可以代入Huffman树进行解码。(代入Huffman树解码就是,从根节点开始,如果0则寻找其左孩子,如果1则寻找其右孩子,找到一个叶子结点就是解码了一个字符,然后从根节点重新开始寻找)

前缀码

Huffman编码是一种不等长编码。不等长编码要想不解码错误就要采用前缀码。

前缀码:没有码字是其他码字的前缀

什么意思?举个例子:

{A,B,C,D,E}五个字符,使用的频率分别为{35,25,,15,15,10},采用如下编码方案{0,1,01,10,11},对应字符串”AABACD”编码为”00100110”,可以有多种解码方法,可以解码为”ACAAEA”,也可以解码为AADCD”。(这种解码不能保证解码的唯一性)

所以,我们需要解码方案中的任何一个码字都不是其他码字的前缀。也即采用前缀码。

回过头看我们上次构建的Huffman树:

我们采用这种左0右1的编码方案,其实就是天然的前缀码。

思考:这里是左0右1编码,那能不能是左1右0?

——可以,Huffman编码的关键在于每个字符的编码都是唯一的,并且没有前缀相同的情况,因此左1右0和左0右1在本质上没有区别,只是编码表示的方式不同。

代码实现

代码思路

先思考,树的结点应该包括哪几部分:

可以参考上面建好的树

树的结点:字符 + 左孩子指针 + 右孩子指针

其中: 左右孩子指针不用说,是为了构建树的数据结构

字符是用于编码,自然是一个字符对应一种编码。

那为什么不保存字符的频率

——回顾Huffman建树的逻辑:获取到字符的频率后,将其排序。从排序后的集合中,连续取出两个元素建树,直到树构建完毕。

所以频率是决定了字符存储在树中的位置,但不需要保存。(如果这样还不清晰,继续看,实践出真知)

构建Huffman树的细节:

1、我们需要获取每个字符的频率:

考虑ASCII码用一个字符型就可表示,这里直接采用一个256长度的整型数组保存字符频率。

这里声明形式:int frequency[256] = {0};

假如获取了一串字符“ayuan":

由于a的ASCII码是97,所以我们是想要frequency[97]++;

同理,对"yuan",也是。

那么,在获取到一个字符后(这里假设为achar,只需要将其转成ASCII码,然后自增即可,也即:

frequency[ (unsigned int )achar ] ++;

2、给每个字符的频率进行排序。

排序的方法有很多种。

由于我们选择用256个字符的数组表示频率,下标映射为字符。如果原地排序,那么下标就会被打乱,字符信息就丢失了,所以这种思路不可取。

那既然我们后面都是要将字符打包成叶子结点的,这里就直接对requency数组处理,这一步就将其打包成叶子结点并实现排序操作

实现思路:

(1)创建一个队列(该队列用于保存Huffman树的叶子结点)

定义为:

(2)将frequency数组的字符symbol入队,每次依据字符的频率排序。由于Huffman树就是先取权重最小的,那我们这也是升序排序。

数据结构

包括树与队列

//树结构定义

typedef struct _htNode //树的结点

{

char symbol;

struct _htNode *left ,*right;

}htNode;

typedef struct _htTree //树

{

htNode *root;

}htTree;

//队列结构定义

#define TYPE htNode*

#define MAX_SZ 256

typedef struct _pQueueNode //队列的成分有解码字符与优先级和指针组成

{

TYPE val; //所有被加入到队列中的结点都是叶子结点

unsigned int priority; //优先级,其实就是字符频率

struct _pQueueNode *next;

}pQueueNode;

typedef struct _pQueue

{

pQueueNode *first; //队列头指针,这里指向第一个pQueueNode

unsigned int size;

/*

* 当最后队列只有一个元素时,这个数据就是Huffman树的根节点

* 所以这个size帮我们判断树的构建进度

*/

}pQueue;

队列相关操作(代码实现)

队列的初始化

创建队列:

初始时,队列中还没有结点,first指向空,size设为0

//queue指针指向队列,所以是传入的*queue,这里需要获取*queue的读写权限

void initPQueue(pQueue **queue)

{

(*queue) = (pQueue*)malloc(sizeof (pQueue));

(*queue) -> first = NULL;

(*queue) -> size = 0;

}

入队

思路:从frequency数组获取字符与频率信息,根据频率判断插入到队列哪个位置

分析:

(1)如果队列的size超过256了,那一定异常了,return;

(2)如果队列为空或者插入元素优先级小于队列第一个,直接插入

(3)比对priority:

假设,现在要插入一个priority为9的数据:

那就首先会将9与8比对,发现9>8,继续比对,发现9不大于10,就将priority为9的数据插入到8之后,10之前:

否则插入到最后。

(4)队列中最后一个元素是Huffman树的根节点。

假设当队列中只剩下三个元素了,且且设优先级为8、9、10

那么取出队列前两个,并入队父节点

继续取两个元素后入队父节点:

此时,(*pQueue)->size == 1,剩下的这个就是根节点

代码实现:

void addQueue(pQueue **queue,TYPE val,unsigned int priority)

{

if((*queue)->size == MAX_SZ)

{

printf("队列已满!");

return ;

}

//生成一个结点

pQueueNode *pNode = (pQueueNode *) malloc(sizeof (pQueueNode));

pNode -> val = val;

pNode -> priority = priority;

if( (*queue) ->size == 0 || priority <= (*queue)->first->priority || (*queue) -> first == NULL) //如果队列为空或者插入元素优先级小于队列第一个,直接插入

{

//注意,由于当队列为空时,结点未初始化,访问(*queue)->first->priority是不存在的,所以先(*queue) ->size == 0后priority <= (*queue)->first->priority

pNode->next = (*queue)->first;

(*queue) -> first = pNode;

(*queue) -> size++;

return;

}

else

{

pQueueNode *iterator = (*queue)->first;

//要么插入到队列中,要么插入到队列末尾

//要么插入到队列中,要么插入到最后

while (iterator->next != NULL)

{

//迭代

if(priority <= iterator->next->priority)

{

pNode->next = iterator->next;

iterator->next = pNode;

(*queue)->size++;

return;

}

iterator = iterator->next;

}

if(iterator->next == NULL)

{

pNode -> next = NULL;

iterator->next = pNode;

(*queue)->size++;

return;

}

/*/*迭代法2

//由于priority大在后,小在前,则一直遍历到有一个比pNode的priority大的,再将该节点插入到此之前,否则插入到最后

for (iterator = (*queue)->first; iterator->next != NULL && pNode->priority > iterator->next->priority; )

{

// 迭代

iterator = iterator->next

}

pNode->next = iterator->next;

iterator->next = pNode;

(*queue)->size++;

*/

}

}

其中:

迭代这里看了两种方法:

(1)第一种,将插入到中间和最后视为两种:

在iterator->next == NULL之前就满足priority <= iterator->next->priority,则将数据插入,否则就插入到最后

//要么插入到队列中,要么插入到最后

while (iterator->next != NULL)

{

//迭代

if(priority <= iterator->next->priority)

{

pNode->next = iterator->next;

iterator->next = pNode;

(*queue)->size++;

return;

}

iterator = iterator->next;

}

if(iterator->next == NULL)

{

pNode -> next = NULL;

iterator->next = pNode;

(*queue)->size++;

return;

}

(2)第二种:对待插入数据,就算在遍历一整个队列后没找到比插入数据结点priority大的数据,插入数据也到了队列末尾,将其插入末尾即可。

for (iterator = (*queue)->first; iterator->next != NULL && pNode->priority > iterator->next->priority; )

{

// 迭代

iterator = iterator->next

}

pNode->next = iterator->next;

iterator->next = pNode;

(*queue)->size++;

*/

出队

从队列中取出叶子结点(用于建树)

//这里我选择在外部判断size,而不在该函数内,故外部一定要检查*pQueue->size

TYPE getPQueue(pQueue **pQueue)

{

TYPE leafVal; //用leafVal返回队列中的值作为叶子结点

//每次移动游标然后free就行

leafVal = (*pQueue)->first->val;

(*pQueue) -> first = (*pQueue)->first->next;

(*pQueue)->size--;

return leafVal;

构建Huffman树(代码实现)

之前分析过的就不多说,构建Huffman树的主要过程:

1、获取字符频率

2、将字符和字符频率入队

3、从队列中连续取出两个叶子结点构建Huffman树,直到队列中无元素(队列的最后一个元素是根节点)

//建树:

/*

* 从外部获取信息,由每个字符的频率创建队列

* 再依据队列建树

*/

void buildTree(htTree *huffmanTree,char *encodeString)

{

//1、首先,获取字符的频率

//我们所有的编码都可以用ASCII码表示,而ASCII码有256个,所以我们需要一个大小为256(MAX_SZ)的整型数组

int frequency[MAX_SZ] = {0}; //初始化为0

//然后获取字符串中每一个字符的频率——从字符串的头,到字符串的尾(\0)

for(int i = 0;encodeString[i] != 0;i++)

{

frequency[ (unsigned int )encodeString[i] ] ++;

}

//2、创建队列

pQueue *huffmanQueue;

initPQueue(&huffmanQueue);

//遍历数组,从中取出所有非零的数(如果没出现,没必要为其进行建树、编码)

//由于我们已经写好了addQueue函数,只需要在遍历的时候调用该函数就行就行

//不过,最好是养成测试的习惯

for(int i = 0;i

{

//队列中的元素为htNode,包含多代表的字符和左右孩子

//所有被加入到队列的结点都是Huffman的叶子结点

//创建结点并入队

if(frequency[i])

{

htNode *leafNode = (htNode*) malloc(sizeof (htNode));

leafNode->symbol = (char)i;

leafNode -> left = leafNode->right = NULL;

addQueue(&huffmanQueue,leafNode,frequency[i]);

}

}

/*

//测试队列的创建是否有问题

for(pQueueNode *testPrintQ = huffmanQueue -> first; testPrintQ;testPrintQ = testPrintQ->next)

{

printf("值为%c的优先级为%d\n",testPrintQ->val->symbol,testPrintQ->priority);

}

printf("队列的大小为%d",huffmanQueue->size);

*/

//3、重头戏之建树,Huffman大叔的精华思想

//只要队列中还剩下结点,就建树,不过最后一个结点是根节点(在不停的出队与入队中,剩下的最后一个只会是根节点)

while(huffmanQueue->size > 1)

{

int priority = huffmanQueue->first->priority + huffmanQueue -> first->next->priority;

//生成中间结点作为头两个队列的双亲

htNode *inNode = (htNode *) malloc(sizeof (htNode));

inNode->left = getPQueue(&huffmanQueue);

inNode->right = getPQueue(&huffmanQueue);

//取出队列中两个元素,权重(这里是文字的出现频率也即优先级队列中结点的priority),两个相加,又入队(这样省的比较了)

addQueue(&huffmanQueue,inNode,priority); //入队

}

huffmanTree -> root = getPQueue(&huffmanQueue);

}

测试队列

为了校验队列创建是否正确,编写测试代码(因为这里队列就是在buildTree中创建的,所以测试代码就在buildTree函数中):

//测试队列的创建是否有问题

for(pQueueNode *testPrintQ = huffmanQueue -> first; testPrintQ;testPrintQ = testPrintQ->next)

{

printf("值为%c的优先级为%d\n",testPrintQ->val->symbol,testPrintQ->priority);

}

printf("队列的大小为%d",huffmanQueue->size);

那这里就测试一下,在main函数中调用试试:

int main()

{

//测试队列,这里采用的文本是"Xiao Yuan ding feng zuo an"

char encodeString[] = "Xiao Yuan ding feng zuo an";

//传入文本,构建Huffman树

htTree huffmanTree;

buildTree(&huffmanTree,encodeString);

return 0;

}

输出结果:

测试队列的优先级,看两个方面,一个是优先级(字符频率),一个是优先级的排序。

这里没问题。

测试Huffman树

因为上面已经输出了队列,可以直接根据队列的逻辑画出Huffman树。

偷个懒,我不画了。

下面是输出Huffman所有叶子结点的测试函数。

//遍历树,输出叶子结点的字符

void testSymbol(htNode *root)

{

if(root == NULL)

{

return;

}

if(root->left == NULL && root->right == NULL)

{

printf("%c",root->symbol);

}

else

{

testSymbol(root->left);

testSymbol(root->right);

}

}

为Huffman树进行编码(代码实现)

上面已经创建好了Huffman树,接下来只需要为Huffman树进行编码即可。

建表

保存形式:字符--码

像上面创建的:

ABCDE001001110111

表声明如下:

typedef struct _hlNode //表的结点

{

char symbol;

char *code;

struct _hlNode *next;

}hlNode;

typedef struct _hlTable //表

{

hlNode *first;

hlNode *last;

}hlTable;

编码逻辑

回顾一下Huffman编码逻辑:从根节点开始,左孩子则0,右孩子则1,直到遇到叶子结点,结束当前叶子结点的编码。

既然只需要保存字符和编码就行,字符不是什么大问题,主要还是考虑保存编码,因为每一次向下递归,就要给字符串多加一个0或1。

由于ASCII码不超过256个,那么树的深度也不超过256个。那么直接用一个长度为256的char型数组一定能容纳下任何一个叶子结点的编码。

也即需要一个:

char code[256];

不过这个code只是个辅助记录的编码,因为在遍历的时候不知道这个叶子的深度,也就不知道这个叶子的编码多长。但是最后保存的编码长度是确定的。所以表的定义中是char *code;

另外,需要一个索引指示编码进行到哪一步了,也即编码到第几个数字了。

先创建一个建表的函数,对表初始化,并将表的修改权限交给traverseTree函数。traverseTree函数主要就负责编码过程。等下再完善这个函数。

//建表

hlTable *buildTable(htTree *huffmanTree)

{

//表的初始化

hlTable *table = (hlTable *) malloc(sizeof(hlTable));

table->first = NULL;

table->last = NULL;

char code[256];

int k = 0; //没有k的话,不知道编码到第一个数字了

traverseTree(huffmanTree->root,&table,k,code); //遍历树,完善表

return table;

}

现在来完善traverseTree函数。

几个注意的点:

1、遇到叶子结点则当前遍历完毕,继续下一次遍历

2、我们用char code[256]保存遍历的编码,但是最后要把结果保存在hlNode的code中

代码实现如下:

void traverseTree(htNode *root,hlTable **codetable,int k,char code[256])

{

if(root->left == NULL &&root->right == NULL) //如果是叶子

{

code[k] = '\0'; //结束字符串

hlNode *aux = (hlNode*) malloc(sizeof (hlNode));

aux->code = (char *) malloc(sizeof (char)*(strlen(code)+1));

strcpy(aux->code,code);

aux->symbol = root->symbol;

aux->next = NULL;

if((*codetable)->first == NULL)

{

(*codetable)->first = aux;

(*codetable)->last = aux;

}

else

{

(*codetable)->last->next = aux;

(*codetable)->last = aux;

}

}

//0、1编码部分

if(root->left != NULL)

{

code[k] = '0';

traverseTree(root->left,codetable,k+1,code);

//k+1,使得可持续向下迭代,为其编码,下同

}

if(root->right != NULL)

{

code[k] ='1';

traverseTree(root->right,codetable,k+1,code);

}

}

测试建表

康康建表有什么问题吗。

测试代码:

void testCodeTable(hlTable *codeTable)

{

hlNode* testNode;

//测试一下表codeTable

for( testNode = codeTable->first;testNode;testNode = testNode->next)

{

printf("%c的编码是%s\n",testNode->symbol,testNode->code);

}

}

要想知道对不对,可以测试一个简单点文本,然后根据上面的逻辑把树画出来,然后就知道编码对不对了。

这个任务交给你们(之前测试画的图找不到了)。

不过"Xiao Yuan ding feng zuo an"的测试输出如下,这是没毛病的。

编码

之前已经建好了表,那么编码只需要获取到字符串,然后比对码表里的编码就行。

还是之前的例子:

表如下——

ABCDE001001110111

文本是“DDACEB”。

取出文本中的每一个字符,在表中寻找对应编码

对D,比对表,发现是110,现在解码出110

对D,比对表,发现是110,现在解码出110110

对A,比对表,发现是00;现在解码出11011000

...总之,最后解码出:110110000111110

实现代码:

void encoding(hlTable *codeTable,char encodeString[])

{

for(int i = 0;encodeString[i] != '\0';i++)

{

//编码思路,遍历encodeString,为所有的字符寻找对应的编码

for (hlNode* searchNode = codeTable->first;searchNode; searchNode = searchNode->next)

{

if(encodeString[i] == searchNode->symbol)

{

printf("%s",searchNode->code); //如果想比对正确与否,只需要调整格式

}

}

}

}

解码

还是之前的例子:

则Huffman编码表:

ABCDE001001110111

试试能不能给出一段码文然后进行解码:

11001011000111110

有没有发现,我们既可以将这段码文代入编码表中进行解码,也可以代入Huffman树进行解码。(代入Huffman树解码就是,从根节点开始,如果0则寻找其左孩子,如果1则寻找其右孩子,找到一个叶子结点就是解码了一个字符,然后从根节点重新开始寻找)

对计算机来说:如果拿表去解码也可以,但是很麻烦。所以借用Huffman树解码。

实现代码:

//解码的过程很清晰

//遍历字符串,遇0做递归左,遇1则递归右,遇到叶子结点则输出当前结点的symbol,持续递归直到字符串结束

void decoing(htNode *root,char decodeString[] )

{

htNode *saveRoot = root;

for(int i = 0;decodeString[i] != '\0';)

{

if(root->left == NULL && root->right == NULL)

{

printf("%c",root->symbol); //如果是叶子结点,输出当前字符

root = saveRoot; //回到根节点继续搜寻

}

else if(decodeString[i] == '0') //否则递归遍历

{

root = root->left;

i++;

}

else if(decodeString[i] == '1')

{

root = root->right;

i++;

}else return;

}

}

请点个小心心。

完整代码

代码整体设计参考哔哩哔哩:鱼C-小甲鱼https://www.bilibili.com/video/BV1jW411K7yg/?p=52&vd_source=49259db5ae68176a71bf94306151ef1ehttp://xn--tss843a0xaz60i

包含测试代码:

#include "stdio.h"

#include "stdlib.h"

#include "string.h"

//树结构定义

typedef struct _htNode //树的结点

{

char symbol;

struct _htNode *left ,*right;

}htNode;

typedef struct _htTree //树

{

htNode *root;

}htTree;

typedef struct _hlNode //表的结点

{

char symbol;

char *code;

struct _hlNode *next;

}hlNode;

typedef struct _hlTable //表

{

hlNode *first;

hlNode *last;

}hlTable;

//队列结构定义

#define TYPE htNode*

#define MAX_SZ 256

typedef struct _pQueueNode //队列的成分有解码字符与优先级和指针组成

{

TYPE val; //所有被加入到队列中的结点都是叶子结点

unsigned int priority; //优先级,由频率决定,与在Huffman中到根节点的路径长度有关

/*

* 如果确定priority是非负数,则用unsigned int

* 否则可能可以利用缓冲区溢出等手段来修改参数达到不可告人的目的

*/

struct _pQueueNode *next;

}pQueueNode;

typedef struct _pQueue

{

pQueueNode *first; //队列头指针,这里指向第一个pQueueNode

unsigned int size;

/*

* 队列长度,时不时就要从里面取出数据、塞入数据以更新频率,所以有个size最好,

* 当最后队列只有一个元素时,这个数据就是Huffman树的根节点

*/

}pQueue;

//queue指针指向队列,所以是传入的*queue,这里需要获取*queue的读写权限

void initPQueue(pQueue **queue)

{

(*queue) = (pQueue*)malloc(sizeof (pQueue));

(*queue) -> first = NULL;

(*queue) -> size = 0;

}

void addQueue(pQueue **queue,TYPE val,unsigned int priority)

{

if((*queue)->size == MAX_SZ)

{

printf("队列已满!");

return ;

}

//生成一个结点

pQueueNode *pNode = (pQueueNode *) malloc(sizeof (pQueueNode));

pNode -> val = val;

pNode -> priority = priority;

if( (*queue) ->size == 0 || priority <= (*queue)->first->priority || (*queue) -> first == NULL) //如果队列为空或者插入元素优先级小于队列第一个,直接插入

{

//注意,由于当队列为空时,结点未初始化,访问(*queue)->first->priority是不存在的,所以先(*queue) ->size == 0后priority <= (*queue)->first->priority

pNode->next = (*queue)->first;

(*queue) -> first = pNode;

(*queue) -> size++;

return;

}

else

{

pQueueNode *iterator = (*queue)->first;

//要么插入到队列中,要么插入到队列末尾

//要么插入到队列中,要么插入到最后

while (iterator->next != NULL)

{

//迭代

if(priority <= iterator->next->priority)

{

pNode->next = iterator->next;

iterator->next = pNode;

(*queue)->size++;

return;

}

iterator = iterator->next;

}

if(iterator->next == NULL)

{

pNode -> next = NULL;

iterator->next = pNode;

(*queue)->size++;

return;

}

/*/*迭代法2

//由于priority大在后,小在前,则一直遍历到有一个比pNode的priority大的,再将该节点插入到此之前

for (iterator = (*queue)->first; iterator->next != NULL && pNode->priority > iterator->next->priority; )

{

// 迭代

iterator = iterator->next

}

pNode->next = iterator->next;

iterator->next = pNode;

(*queue)->size++;

*/

}

}

//这里我选择在外部判断size,而不在该函数内,故外部一定要检查*pQueue->size

TYPE getPQueue(pQueue **pQueue)

{

TYPE leafVal; //用leafVal返回队列中的值作为叶子结点

//每次移动游标然后free就行

leafVal = (*pQueue)->first->val;

(*pQueue) -> first = (*pQueue)->first->next;

(*pQueue)->size--;

return leafVal;

}

//建树:

/*

* 从外部获取信息,由每个字符的频率创建队列

* 再依据队列建树

*/

void buildTree(htTree *huffmanTree,char *encodeString)

{

//1、首先,获取字符的频率

//我们所有的编码都可以用ASCII码表示,而ASCII码有256个,所以我们需要一个大小为256(MAX_SZ)的整型数组

int frequency[MAX_SZ] = {0}; //初始化为0

//然后获取字符串中每一个字符的频率——从字符串的头,到字符串的尾(\0)

for(int i = 0;encodeString[i] != 0;i++)

{

frequency[ (unsigned int )encodeString[i] ] ++;

}

//2、创建队列

pQueue *huffmanQueue;

initPQueue(&huffmanQueue);

//遍历数组,从中取出所有非零的数(如果没出现,没必要为其进行建树、编码)

//由于我们已经写好了addQueue函数,只需要在遍历的时候调用该函数就行就行

//不过,最好是养成测试的习惯

for(int i = 0;i

{

//队列中的元素为htNode,包含多代表的字符和左右孩子

//所有被加入到队列的结点都是Huffman的叶子结点

//创建结点并入队

if(frequency[i])

{

htNode *leafNode = (htNode*) malloc(sizeof (htNode));

leafNode->symbol = (char)i;

leafNode -> left = leafNode->right = NULL;

addQueue(&huffmanQueue,leafNode,frequency[i]);

}

}

/*

//测试队列的创建是否有问题

for(pQueueNode *testPrintQ = huffmanQueue -> first; testPrintQ;testPrintQ = testPrintQ->next)

{

printf("值为%c的优先级为%d\n",testPrintQ->val->symbol,testPrintQ->priority);

}

printf("队列的大小为%d",huffmanQueue->size);

*/

//3、重头戏之建树,Huffman大叔的精华思想

//只要队列中还剩下结点,就建树,不过最后一个结点是根节点(在不停的出队与入队中,剩下的最后一个只会是根节点)

while(huffmanQueue->size > 1)

{

int priority = huffmanQueue->first->priority + huffmanQueue -> first->next->priority;

//生成中间结点作为头两个队列的双亲

htNode *inNode = (htNode *) malloc(sizeof (htNode));

inNode->left = getPQueue(&huffmanQueue);

inNode->right = getPQueue(&huffmanQueue);

//取出队列中两个元素,权重(这里是文字的出现频率也即优先级队列中结点的priority),两个相加,又入队(这样省的比较了)

addQueue(&huffmanQueue,inNode,priority); //入队

}

huffmanTree -> root = getPQueue(&huffmanQueue);

}

//遍历树,输出叶子结点的字符

void testSymbol(htNode *root)

{

if(root == NULL)

{

return;

}

if(root->left == NULL && root->right == NULL)

{

printf("%c",root->symbol);

}

else

{

testSymbol(root->left);

testSymbol(root->right);

}

}

void traverseTree(htNode *root,hlTable **codetable,int k,char code[256])

{

if(root->left == NULL &&root->right == NULL) //如果是叶子

{

code[k] = '\0'; //结束字符串

hlNode *aux = (hlNode*) malloc(sizeof (hlNode));

aux->code = (char *) malloc(sizeof (char)*(strlen(code)+1));

strcpy(aux->code,code);

aux->symbol = root->symbol;

aux->next = NULL;

if((*codetable)->first == NULL)

{

(*codetable)->first = aux;

(*codetable)->last = aux;

}

else

{

(*codetable)->last->next = aux;

(*codetable)->last = aux;

}

}

//0、1编码部分

if(root->left != NULL)

{

code[k] = '0';

traverseTree(root->left,codetable,k+1,code);

//k+1,使得可持续向下迭代,为其编码,下同

}

if(root->right != NULL)

{

code[k] ='1';

traverseTree(root->right,codetable,k+1,code);

}

}

//建表

hlTable *buildTable(htTree *huffmanTree)

{

//表的初始化

hlTable *table = (hlTable *) malloc(sizeof(hlTable));

table->first = NULL;

table->last = NULL;

char code[256];

int k = 0;

traverseTree(huffmanTree->root,&table,k,code); //遍历树,完善表

return table;

}

void testCodeTable(hlTable *codeTable)

{

hlNode* testNode;

//测试一下表codeTable

for( testNode = codeTable->first;testNode;testNode = testNode->next)

{

printf("%c的编码是%s\n",testNode->symbol,testNode->code);

}

}

void encoding(hlTable *codeTable,char encodeString[])

{

for(int i = 0;encodeString[i] != '\0';i++)

{

//编码思路,遍历encodeString,为所有的字符寻找对应的编码

for (hlNode* searchNode = codeTable->first;searchNode; searchNode = searchNode->next)

{

if(encodeString[i] == searchNode->symbol)

{

printf("%s",searchNode->code); //如果想比对正确与否,只需要调整格式

}

}

}

}

//解码的过程很清晰

//遍历字符串,遇0做递归左,遇1则递归右,遇到叶子结点则输出当前结点的symbol,持续递归直到字符串结束

void decoing(htNode *root,char decodeString[] )

{

htNode *saveRoot = root;

for(int i = 0;decodeString[i] != '\0';)

{

if(root->left == NULL && root->right == NULL)

{

printf("%c",root->symbol); //如果是叶子结点,输出当前字符

root = saveRoot; //回到根节点继续搜寻

}

else if(decodeString[i] == '0') //否则递归遍历

{

root = root->left;

i++;

}

else if(decodeString[i] == '1')

{

root = root->right;

i++;

}else return;

}

}

int main()

{

system("chcp 65001");

char encodeString[] = "Xiao Yuan ding feng zuo an";

htTree huffmanTree;

buildTree(&huffmanTree,encodeString);

/*

//那就在这里遍历测试一下buildTree

printf("测试输出Huffman树的所有叶子结点的字符(注意输入的文档里面有空格)\n");

testSymbol(huffmanTree.root);

*/

hlTable *codeTable = buildTable(&huffmanTree);

/*

//测试一下表格

testCodeTable(codeTable);

*/

//编码

printf("\nencodeString的编码:\n");

encoding(codeTable,encodeString);

char decodeString[] = "111101001110010001111111000110011001010110000100011111010010111110001011110100110010001011011001111101110";

printf("\n\n解码:\n");

decoing(huffmanTree.root,decodeString);

return 0;

}

相关文章

365结束投注 怎么把文字和图片编辑在一起?教你几招将文字融入图片
彩票365苹果版怎么下载不了 六款成年人必备软件测评盘点,点赞收藏!
彩票365苹果版怎么下载不了 蔽的意思,蔽的解释,蔽的拼音,蔽的部首,蔽的笔顺
365体育投注网址亚洲下载 手机信号屏蔽方法及注意事项科普
365体育投注网址亚洲下载 一三一素酵素 综合水果酵素粉黑金藻酵素6g*12包
彩票365苹果版怎么下载不了 值得收藏的14个公开课网站

值得收藏的14个公开课网站

🗓️ 07-18 👁️ 8886