符号表结构体:

struct node
{
// 字符串形式存储的Huffman编码
char code[MAX_CODE_LENGTH];
// 这个字符在文件中出现的次数
long count;
// 在生成Huffman树的时候是否已经被当作叶子节点
int checked;
// 符号
char sym;
// left和right只在生成Huffman树的时候有用
struct node* next,*left,*right;
};

全局变量:

const int BIT_WIDTH_CHAR = 8;
const int END_SYM_FLAG = 200;// 符号的范围是-127~128
int gl_total_node_num = 0;// 符号表的总长度
int gl_source_file_length = 0;// 源文件的总长度

辅助函数:

// 在链表中查找指定的字符
// 参数:符号表
// 参数:字符
// 返回值:找到的节点的指针
struct node* content_char(struct node*,char);
// 在链表中查找指定编码
// 参数:符号表
// 参数:编码
// 返回值:找到的节点的指针
struct node* content_code(struct node* list,const char* ch);
// 根据字符创建一个新的节点
// 参数:字符
// 参数:计数
// 返回值:新节点指针
struct node* create_new_node(char ch,int count);
// 插入新节点到符号表的最前面
// 参数list:目标链表
// 参数new_node:新节点
// 返回值:插入后的链表
struct node* insert_node(struct node *list,struct node *new_node);
// 输出链表
// 参数list:目标链表
void print_list(struct node *list);
// 获取到最小的未被检查的count的节点,返回它的指针,并将其设置为检查过的
// 参数list_addr:链表头
// 返回值:第一个未检查的最少出现次数的结点指针
struct node* get_smallest_node(struct node *list_addr);

压缩:

第一步:建立符号表

在main函数中:获取文件的总长度,用于生成进度条

// 获取文件长度,以实现进度条
fseek(source_file,0,SEEK_END);
gl_source_file_length = ftell(source_file);
fseek(source_file,0,SEEK_SET);

扫描源文件,按字符读取,建立符号表,统计每个字符出现的次数

首先提示进度条,10个'.'为结束

将读取到的字符在已有的符号表中查找,如果已存在就将该节点的count+1,否则就创建新节点并插入到符号表中

统计符号表中总节点数

// 生成符号链表(含频率)
// 参数:源文件
// 参数:目标链表
// 返回值:符号链表
struct node* generate_count(FILE* f,struct node*list)
{
printf("counting");
int count=0; char ch;
struct node *content_node;
while(fread(&ch,sizeof(char),1,f)==1)
{
// 进度条
count++;
if(count%(gl_source_file_length/10+1)==0)
printf("."); // 插入符号表
content_node = content_char(list,ch);
if(content_node)
content_node->count++;
else
{
gl_total_node_num++;
list=insert_node(list,create_new_node(ch,1));
}
}
printf("\n");
return list;
}

第二步:生成Huffman树

生成Huffman树

使用先前统计的符号表总数进行计数循环,因为生成树的所有非叶子节点共有叶子节点-1个

首先输出进度条

视初始时所有的符号表中的节点为只有一个节点的子树,获取两个最小的count的子树根节点指针,这是通过节点的checked属性实现的,如果使用了两个子树根节点生成新的子树,那么这两个子树根节点的checked将会设为1,在寻找最小节点的时候将会跳过

将新的树的中间节点插入到符号表中

最终的效果是符号表中只有最前面的一个节点的checked为0

// 生成Huffman树
// 参数:符号链表
// 返回值:含有Huffman树的链表,非叶节点将插入到链表前面,并有左右孩子属性,叶节点没有左右孩子属性
struct node* generate_tree(struct node* list)
{
printf("generate_tree");
// 生成树
for(int i=1;i<gl_total_node_num;i++)
{
// 进度条
if(i%(gl_total_node_num/10+1)==0)
printf("."); // 获取最小出现次数的两个符号,并生成新的节点插入符号表
struct node *sm_left=get_smallest_node(list);
struct node *sm_right=get_smallest_node(list);
struct node *new_node=create_new_node('\0',sm_left->count+sm_right->count);
new_node->left=sm_left;
new_node->right=sm_right; list = insert_node(list,new_node);
}
printf("\n");
return list;
}

第三步:生成编码

从Huffman树中生成符号表的编码属性

因为这是一棵树,所以用递归的方式进行

某节点的左孩子的编码将在继承其父节点编码的基础上扩展'0',右孩子将在继承其父节点的编码的基础上扩展'1'

最终所有叶子节点的编码就是Huffman编码

// 递归的生成Huffman编码于符号链表
// 参数:Huffman树
void generate_code(struct node*list)
{
// 生成Huffman编码
if(!list)return;
// 左子树扩展'0'
if(list->left)
{
strcat(list->left->code,list->code);
strcat(list->left->code,"0");
generate_code(list->left);
}
// 右子树'1'
if(list->right)
{
strcat(list->right->code,list->code);
strcat(list->right->code,"1");
generate_code(list->right);
}
}

因为之后树就没什么用了,只有叶子节点有用,所以将其他节点释放掉

// 释放Huffman树的非叶子节点,只留下符号链表
// 参数:Huffman树
// 返回值:不含Huffman树的符号链表
struct node* free_tree(struct node*list)
{
struct node*free_node=list;
while(list->left && list->right)// 左右子树不为空的节点都是需要释放的
{
free_node=list;
list=list->next;
free(free_node);
}
return list;
}

第四步:生成目标文件

首先将符号表写入到目标文件头部,再次扫描源文件,将每个字符转换为对应的编码,并以二进制的形式存储在目标文件中

// 生成目标文件
// 参数:源文件
// 参数:目标文件
// 参数:符号链表
void generate_des_file(FILE* sf,FILE* df,struct node*list)

首先,为了解压,要把符号表的内容写入到目标文件前面

其中符号和编码时两两配对的,最后通过一个符号表不可能取到的值标记结束

标识符之间是通过空格隔开,最终结束时以换行符结尾。这里的规则将在解压的时候需要严格遵守

// 符号表以文本形式写入到目标文件的前端,解压时需要的信息
struct node* index=list;
while(index)
{
fprintf(df,"%d %s ",index->sym,index->code);
index=index->next;
}
// 指示结尾,"END"实际上没有用到,只用于和END_SYM_FLAG配对
fprintf(df,"%d %s\n",END_SYM_FLAG,"END");

变量:

// 实际文件内容(二进制形式)
// 前者是从源文件中读取的字符,后置是对Huffman编码进行二进制形式转换后取8位形成的字符
char ch,des_ch='\0';
// 目标字符知否足够8位可以进行写入?
int des_ch_length=0;
// 因为之前进行了读取,所以这里回到文件头
fseek(sf,0,SEEK_SET);
int count=0;// 程序执行进度提示

while循环读文件:

while(fread(&ch,sizeof(char),1,sf)==1)

输出进度条,并根据从源文件中读取的字符找符号表中对应的节点

// 进度条
count++;
if(count%(gl_source_file_length/10+1)==0)
printf("."); // 在符号表中找这个字符
// 没有找到一定是出错了
struct node *content_node = content_char(list,ch);
if(!content_node)
{
printf("error:cannot match with sym list\n");
exit(0);
}

现在需要将符号对应的字符串Huffman编码转化为一个个8位char类型,其中每一位代表一位Huffman编码

对这个编码每一位循环处理:将已经部分生成的目标字符左移一位,如果当前的编码位为1就用掩码0000 0001和目标字符按位或,那么最后一位将为1,其余不变,当前编码位为0就不做操作,因为本来左移就补0

当记录的长度达到8的时候就将这个字符写入到目标文件,将记录长度清零,继续对编码的每一位循环

// 对这个符号对应的Huffman编码进行二进制转化
char* current_code=content_node->code;
for(int i=0;i<strlen(current_code);i++)
{
// 每次循环左移一位,长度+1
des_ch=des_ch<<1;
des_ch_length++;
// 末位默认位0,否则置1
if(current_code[i]=='1')des_ch |= (char)1; // 已经足够了一个字符,就写入,并清0
if(des_ch_length==8)
{
if(!fwrite(&des_ch,sizeof(char),1,df))
{
printf("error:cannot write to des file.\n");
exit(0);
}
des_ch_length=0;
des_ch=0;
}// 形成了一个字符
}// Huffman编码

但是最后一位不一定够8位,这需要额外的说明

将剩下的这个字符左对齐,并在目标文件的最后一个字符说明前一个字符有几位有效

// 最后一个字符,没有满足8位
if(des_ch_length!=0)
{
des_ch=des_ch<<BIT_WIDTH_CHAR-des_ch_length;
if(!fwrite(&des_ch,sizeof(char),1,df))
{
printf("error:cannot write to des file.\n");
exit(0);
}
}
// 最后这个一定是一个字符(1-8),表示最后一个有效字符的长度
fprintf(df,"%d",des_ch_length);

解压:

第一步:从目标文件读取符号表

首先从源文件头读取并创建符号表

值得注意的是读取字符串的时候用fscanf将会在遇到空格的时候结束,而用其他的方式将会在换行符时结束,但是我们在生成的时候全都生成在一行

在读取结束的时候需要将换行符读掉,否则接下来将会读到换行符并解析

// 获取文件头的符号表
// 参数:源文件
// 返回值:符号表
struct node* de_get_sym_list(FILE* f)
{
printf("getting sym list...\n"); char code[MAX_CODE_LENGTH];
int ch_int;
struct node* list=NULL; fscanf(f,"%d %s",&ch_int,code);
// 如果没有读到结束标志
while(ch_int!=END_SYM_FLAG)
{
// 创建新节点并插入符号表
struct node* new_node=create_new_node((char)ch_int,0);
strcpy(new_node->code,code);
list=insert_node(list,new_node);
fscanf(f,"%d %s",&ch_int,code);
}
fgetc(f);// 将换行符读掉,按照生成的规则,最后是一个换行符
return list;
}

第二步:解析压缩内容

// 生成解压后的文件
// 参数:源文件
// 参数:目标文件
// 参数:符号链表
void de_generate_des_file(FILE* sf,FILE* df,struct node* list)

变量:

// 存储未形成一个有效的Huffman编码的字符串
char temp_code[MAX_CODE_LENGTH] = {'\0'};
int last_length;// 最后一个字符的有效位长度
// 分别指向实际的压缩内容(去除头部的符号表)的头和尾
long current_file_index,file_length;
// 位操作的掩码,首位为1其余为0
char mask=((char)1)<<BIT_WIDTH_CHAR-1;
// 用于扩展Huffman编码的字符串,"0"或者"1"
char append[]={'0','\0'};

对文件的长度进行测量,并将最后一个字符有效位数读取进来,如果不记录长度,在之后的循环读取中想要跳过最后一个符号将会有些麻烦

记得要将文件的游标移动到合适的位置

// 记录此时符号表读取结束的位置
current_file_index = ftell(sf);
// 从文件尾获取最后一个字符的长度,以及文件的长度
fseek(sf,-(sizeof(char)),SEEK_END);
file_length = ftell(sf);
last_length = fgetc(sf)-'0';
// 回复当前位置到符号表结束的位置
fseek(sf,current_file_index,SEEK_SET);

因为已知了长度,所以读文件的时候可以计数循环,而不是检测文件尾。跳过最后一个字符(当然,这里的最后一个不包括后面的那个表示它的有效位数的数字字符)

for(int i=current_file_index;i<file_length-1;i++)

首先时进度条,并进行读取:

// 进度条
if(i%((file_length-current_file_index)/10+1)==0)
printf(".");
// 读取是否成功
if(fread(&ch,sizeof(char),1,sf)!=1)
{
if(ferror(sf))
printf("error:cannot read file.\n");
if(feof(sf))
printf("end of reading file.\n");
exit(0);
}

对这个字符的每一位循环,将这个位转化为字符形式扩展到已有的字符串上,每扩展一位就检测一下符号表中有没有这个编码,有了就将对应的字符输出到文件中,并将字符串清空

位操作需要获取的是第一个位,所以掩码是第一位为1其余为0,按位与将获取第一个位:非0为1,0为0

// 将这一个字符的每一位扩展到未竟的Huffman编码上
for(int j=0;j<BIT_WIDTH_CHAR;j++)
{
append[0]='0' + ((ch & mask)==0?0:1);
strcat(temp_code,append);
ch=ch<<1;
// 尝试在符号表中寻找
the_node = content_code(list,temp_code);
if(the_node)
{
// 如果找到了就把其代表的符号写入文件
if(fwrite(&(the_node->sym),sizeof(char),1,df)!=1)
{
printf("error:failed to write file.\n");
exit(0);
}
// 清零临时字符串
temp_code[0]='\0';
}// 如果在符号表中找到
}// 对一个字符的每一位

最后一个字符,利用之前的长度进行循环,而不是8位

// 最后一个字符
if(fread(&ch,sizeof(char),1,sf)!=1)
{
printf("error:cannot read file.\n");
exit(0);
}
for(int i=0;i<last_length;i++)
{
append[0]='0' + ((ch & mask)==0?0:1);
strcat(temp_code,append);
ch=ch<<1;
// 尝试在符号表中寻找
the_node = content_code(list,temp_code);
if(the_node)
{
// 如果找到了就把其代表的符号写入文件
if(fwrite(&(the_node->sym),sizeof(char),1,df)!=1)
{
printf("error:failed to write file.\n");
exit(0);
}
// 清零临时字符串
temp_code[0]='\0';
}// 如果在符号表中找到
}

辅助函数:

// 根据字符创建一个新的节点
// 参数:字符
// 参数:计数
// 返回值:新节点指针
struct node* create_new_node(char ch,int count)
{
struct node *new_node = (struct node*)malloc(sizeof *new_node);
if(!new_node)
{
printf("error:failed to malloc.\n");
exit(0);
}
new_node->code[0]='\0';
new_node->sym=ch;
new_node->count=count;
new_node->next=NULL;
new_node->checked=0;
new_node->left=NULL;
new_node->right=NULL;
return new_node;
}
// 插入新节点
// 参数list:目标链表
// 参数new_node:新节点
// 返回值:插入后的链表
struct node* insert_node(struct node *list,struct node *new_node)
{
if(list)
new_node->next=list;
return new_node;
}
// 获取到最小的未被检查的count的节点,返回它的指针,并将其设置为检查过的
// 参数list_addr:链表头
// 返回值:第一个未检查的最少出现次数的结点指针
struct node* get_smallest_node(struct node *list)
{
while(list && list->checked)list=list->next;// 获取到首个未检查的节点
struct node *smallest=list;
// 获取到最小的count的节点
while(list)
{
if(!list->checked && (list->count < smallest->count))
smallest=list;
list=list->next;
}
if(smallest)smallest->checked++;
return smallest;
}

执行程序:

我用的是Windows10下的gcc编译器,命令行执行,并通过命令行参数传递源文件名和目的文件名

编译:gcc Huffman.c -o Huffman.exe

执行:Huffman.exe sourcefile desfile

执行之后会询问是执行压缩还是解压还是结束程序

执行压缩会把源文件压缩另存为目的文件

解压会将源文件解压另存为目的文件

需要注意的问题:

文件打开的时候是要用二进制流的形式打开,因为要对其进行位操作。如果用文本模式打开,在解压非文本文件的时候在中途程序就可能会认为自己读到的是EOF而结束读取。

因为符号是char类型的字符,所以符号表最大是256,编码的长度最长为255

将char解释为int类型时其取值范围为-127~128,所以想要标记压缩文件中符号表内容的结束,需要用这个范围之外的数

源码地址:

https://github.com/biaoJM/Huffman-Compression

C语言Huffman压缩和解压的更多相关文章

  1. huffman压缩解压文件【代码】

    距离上次写完哈夫曼编码已经过去一周了,这一周都在写huffman压缩解压,哎,在很多小错误上浪费了很多时间调bug.其实这个程序的最关键部分不是我自己想的,而是借鉴了某位园友的代码,但是,无论如何,自 ...

  2. 【C#公共帮助类】WinRarHelper帮助类,实现文件或文件夹压缩和解压,实战干货

    关于本文档的说明 本文档使用WinRAR方式来进行简单的压缩和解压动作,纯干货,实际项目这种压缩方式用的少一点,一般我会使用第三方的压缩dll来实现,就如同我上一个压缩类博客,压缩的是zip文件htt ...

  3. C#文件或文件夹压缩和解压方法(通过ICSharpCode.SharpZipLib.dll)

    我在网上收集一下文件的压缩和解压的方法,是通过ICSharpCode.SharpZipLib.dll 来实现的 一.介绍的目录 第一步:下载压缩和解压的 ICSharpCode.SharpZipLib ...

  4. java 文件压缩和解压(ZipInputStream, ZipOutputStream)

    最近在看java se 的IO 部分 , 看到 java 的文件的压缩和解压比较有意思,主要用到了两个IO流-ZipInputStream, ZipOutputStream,不仅可以对文件进行压缩,还 ...

  5. C#实现通过Gzip来对数据进行压缩和解压

    C#实现通过Gzip来对数据进行压缩和解压 internal static byte[] Compress(byte[] data) { using (var compressedStream = n ...

  6. linux常用命令:4文件压缩和解压命令

    文件压缩和解压命令 压缩命令:gzip.tar[-czf].zip.bzip2 解压缩命令:gunzip.tar[-xzf].unzip.bunzip2 1. 命令名称:gzip 命令英文原意:GNU ...

  7. .net文件压缩和解压及中文文件夹名称乱码问题

    /**************************注释区域内为引用http://www.cnblogs.com/zhaozhan/archive/2012/05/28/2520701.html的博 ...

  8. linux下文件加密压缩和解压的方法

    一.用tar命令 对文件加密压缩和解压 压缩:tar -zcf  - filename |openssl des3 -salt -k password | dd of=filename.des3 此命 ...

  9. Linux下的压缩和解压

    1. gzip, bzip2 能否直接压缩目录呢?不可以 2. 请快速写出,使用gzip和bzip2压缩和解压一个文件的命令.压缩:gzip 1.txt bzip2 1.txt解压:gzip -d 1 ...

随机推荐

  1. day21 模块

    目录 模块 import 与 from...import 循环导入问题 解决方案一 解决方案二 Python文件的两种用途 从普通的面条型代码,到函数型代码,其实是在做什么? 封装代码,一个函数差不多 ...

  2. CF895C Square Subsets (组合数+状压DP+简单数论)

    题目大意:给你一个序列,你可以在序列中任选一个子序列,求子序列每一项的积是一个平方数的方案数. 1<=a[i]<=70 因为任何一个大于2的数都可以表示成几个质数的幂的乘积 所以我们预处理 ...

  3. myeclipse如何取消某一个文件的校验

  4. poj2411 Mondriaan's Dream (状压dp+多米诺骨牌问题)

    这道题的解析这个博客写得很好 https://blog.csdn.net/shiwei408/article/details/8821853 大致意思就是我们可以只处理两行之间的关系,然后通过这两个关 ...

  5. poj 2288 Islands and Bridges (状压dp+Tsp问题)

    这道题千辛万苦啊! 这道题要涉及到当前点和前面两个点,那就设dp[state][i][j]为当前状态为state,当前点为i,前一个点为j 这个状态表示和之前做炮兵那题很像,就是涉及到三个点时,就多设 ...

  6. .net 参数修饰符

    参数修饰符的作用 参数修饰符 作用 无 如果一个参数没有用参数修饰符标记,则认为它将按值传递(pass by value),这意味着被调用的方法收到原始数据的一份副本 out 输出参数由被调用的方法赋 ...

  7. C/s模式与B/S模式

    C/S模式事是client/server,即客服端/服务模式

  8. [Python + Unit Testing] Write Your First Python Unit Test with pytest

    In this lesson you will create a new project with a virtual environment and write your first unit te ...

  9. 上机题目(0基础)-计算两个正整数的最大公约数和最小公倍数(Java)

    题目例如以下:

  10. windowsclient崩溃分析和调试

    本文介绍windows上崩溃分析的一些手段,顺便提多进程调试.死锁等. 1.崩溃分析过程 1.1 确认错误码 不管是用windbg还是用vs.首先应该注意的是错误码,而90%以上的崩溃都是非法訪问. ...