1 哈夫曼编码
1.1 哈夫曼编码定义
规定哈夫曼树的左分支为0, 右分支为1,则从根节点到每个叶子节点经过的分支对应的0和1组成的序列便为该结点字符的编码。这样的编码成为哈夫曼编码。
1.2 哈夫曼编码是一种前缀编码
前缀编码 是指对字符集进行编码时,要求字符集中任意字符的编码都不是其他字符的编码的前缀。
例如:3:0000
23:01
那么3前面绝对不会出现01这样的,就证明哈夫曼编码是一种前缀编码。
1.3 C语言实现哈夫曼编码
1.3.1 比较最小值和次小值
// 寻找最小值和次小值的下标函数
void compare(int *s1, int *s2, Node* ps, int n)
{
int min1 = 9999999, min2 = 9999999, i;
// 获取最小值的下标
for (i = 0; i < 2 * n - 1; i++)
{
if (ps[i].weight != 0 && ps[i].parent == -1) // 如果当前权重不为0,并且父结点为-1,就是孤结点
{
if (min1 > ps[i].weight)
{
*s1 = i; // 获取下标
min1 = ps[i].weight; // 每次比较后获取较小值,进行下一次比较
}
}
}
// 获取次小值的下标
for (i = 0; i < 2 * n - 1; i++)
{
if (ps[i].weight != 0 && ps[i].parent == -1) // 如果当前权重不为0,并且父结点为-1,就是孤结点
{
if (min2 > ps[i].weight && i != *s1)
{
*s2 = i; // 获取下标
min2 = ps[i].weight; // 每次比较后获取较小值,进行下一次比较
}
}
}
}
1.3.2 创建哈夫曼树并初始化
Node *create(int leaf_weight[], const char hanzi[][20], int n) {
int s1, s2;
Node *ps = (Node *)malloc((2 * n - 1) * sizeof(Node));
for (int i = 0; i < 2 * n - 1; i++) {
ps[i].parent = ps[i].lchild = ps[i].rchild = -1;
if (i < n) {
ps[i].weight = leaf_weight[i];
ps[i].name = strdup(hanzi[i]);
} else {
ps[i].weight = 0;
ps[i].name = ""; // 初始化为空字符串
}
}
for (int i = n; i < 2 * n - 1; i++) {
compare(&s1, &s2, ps, n);
ps[i].weight = ps[s1].weight + ps[s2].weight;
ps[s2].parent = ps[s1].parent = i;
ps[i].lchild = s1;
ps[i].rchild = s2;
}
return ps;
}
1.3.3 构建哈夫曼编码并写到文本文件
void writeHuffmanCodeToFile(Node *ps, int n) {
FILE *file;
file = fopen("哈夫曼.txt", "w");
if (file == NULL) {
printf("无法打开文件");
exit(1);
}
else {
for (int i = 0; i < n; i++) {
fprintf(file, "%s:", ps[i].name);
int current = i;
char huffmanCode[256] = ""; // 使用数组来存储哈夫曼编码
while (ps[current].parent != -1)
{
if (current == ps[ps[current].parent].lchild) {//如果该节点是左孩子
strcat(huffmanCode, "0");
}
else if (current == ps[ps[current].parent].rchild) {//如果该节点是右孩子
strcat(huffmanCode, "1");
}
current = ps[current].parent;
}
int len = strlen(huffmanCode);
for (int j = 0; j < len / 2; j++) {//倒置
char temp = huffmanCode[j];
huffmanCode[j] = huffmanCode[len - j - 1];
huffmanCode[len - j - 1] = temp;
}
fprintf(file, "%s\n", huffmanCode);
printf("%s\n", huffmanCode);
}
fclose(file); // 在写入完成后关闭文件
}
}
代码实现,得到的都是叶子节点的权重和哈夫曼编码,所以没有左孩子右孩子节点。
2 压缩
先要读取权重文件和哈夫曼编码文件并存储在数组里,方便对照
// 读取权重文件
void ReadWeight() {
FILE *file1; // 权重文件
// 打开文件
file1 = fopen("权重.txt", "r");
if (file1 == NULL) {
printf("权重文件打开失败");
exit(1);
}
// 逐个读取文件内容并存储
char line[50];
while (fgets(line, sizeof(line), file1) != NULL) {
n++;//为了得到节点数
char *token = strtok(line, ":\n");
strcpy(leaf_name[n], token);//copy文字到数组
token = strtok(NULL, ":\n");
leaf_weight[n] = atoi(token);//把权重放数组
}
// 关闭文件
fclose(file1);
printf("权重文件读取成功\n");
}
// 读取哈夫曼编码
void code_read() {
// 打开文件
FILE *file2 = fopen("哈夫曼编码.txt", "r"); // 以只读方式打开文件
if (file2 == NULL) {
printf("打开文件失败\n"); // 如果文件打开失败,输出错误信息
exit(1); // 退出程序
}
// 读取内容
char line[50]; // 定义一个字符数组来存储每行的文本
while (fgets(line, sizeof(line), file2) != NULL) { // 逐行读取文件内容
line[strcspn(line, "\n")] = '\0'; // 去除换行符
char *token = strtok(line, ":"); // 使用冒号分割每行文本,获取第一部分
strcpy(huf[hu][0], token);//将第一部分存储在 huf 的第一列中
token = strtok(NULL, ":"); // 继续获取下一部分
strcpy(huf[hu++][1], token);//将第二部分存储在 huf 的第二列中,并递增huf的索引
}
// 关闭文件
fclose(file2); // 关闭文件
printf("哈夫曼编码文件读取成功\n"); // 输出成功读取文件的信息
}
压缩为二进制文件,那么就是bin文件,但是为了方便看一看压缩后的文件内容,可以把后缀改为txt查看
以下代码就是对一个字符的哈夫曼编码进行左移操作,以将其添加到正在构建的压缩数据中。
思路:依次查找文本文件的字与权重的字对比,对比正确的按照左移操作对其压缩。
注意:1.为读取的是ANSI文件,那么最多就是三个字符读取到word里面,因为汉字不止一个字符
2.一个字节是8个比特(bit)
3.压缩得到的文件内容是乱码
4.压缩得到的文件绝对比原文件小
void compress() {
// 逐字压缩
char word[3]; // 用于存储从原始文件中读取的字符数组
unsigned char ch; // 用于存储压缩后的字符
int b = 0; // 用于跟踪当前字节中的位数
// 从原始文件中读取最多3个字符到word中
fgets(word, 3, originalfile);//(ANSI文件)
// 循环处理读取的字符
do {
int n1 = 1; // 辅助变量n1的初始化
for (int i = 0; i <= hu; i++) { // 遍历哈夫曼编码表
if (strcmp(huf[i][0], word) == 0) { // 如果找到与word对应的编码
for (int k = 0; k < strlen(huf[i][1]); k++) { // 对编码进行处理
ch += (huf[i][1][k] - '0'); // 按位将编码转换为字符
b++; // 记录处理位数
if (b == 8) { // 如果已经处理了一个完整的字节
fwrite(&ch, sizeof(char), 1, file); // 将字符写入文件
b = 0; // 重置位数
}
ch = ch << 1; // 左移一位
}
memset(word, '\0', sizeof(word)); // 清空word数组
}
n1++; // 增加辅助变量n1的值
}
if (n1 == 1) fprintf(file, "%s,", word); // 如果在哈夫曼编码表中找不到对应的编码,直接将原始字符写入文件
} while (fgets(word, 3, originalfile)); // 继续读取原始文件的内容并进行处理
}
3 解压
压缩就是左移操作,那么解压就是右移操作。
解压得到的是看得懂的字,并非乱码
// 解压得到原文
void decompress(Huffman ps) {
// 逐字解压
int current = 2 * n - 1; // 从哈夫曼树的根节点开始
char bit;
while (fread(&bit, sizeof(char), 1, file) > 0) {
for (int i = 7; i >= 0; --i) {
if ((bit >> i) & 1) { // 读取一个比特
current = ps[current].right; // 如果为1,移动到右孩子
} else {
current = ps[current].left; // 如果为0,移动到左孩子
}
if (current <= n) { // 到达叶子节点,输出对应字符
fprintf(outfile, "%s", ps[current].name);
current = 2 * n - 1; // 从根节点重新开始
}
}
}
}
4 完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//错误点,把三国演义汉字提取到二维数组再与权重文本文件比较得到哈夫曼编码(麻烦)
//strtok分离,strcopy,copy
//压缩对一个字符的哈夫曼编码进行左移操作,以将其添加到正在构建的压缩数据中。
//解压对已经解压的哈夫曼编码进行右移操作,以逐位地获取哈夫曼编码中的每个比特位(bit)
// 定义节点结构体
typedef struct Node {
char name[8]; // 存放汉字字符
int weight; // 存放权重
int left; // 左孩子
int right; // 右孩子
int parent; // 父节点
} Node, *Huffman;
// 全局变量
int n; // 节点个数
char leaf_name[5000][4]; // 节点字符
int leaf_weight[5000]; // 节点权重
char huf[5000][2][30];//二维字符数组
//包含 5000 个元素的二维数组,每个元素都是一个包含两个字符串的数组,每个字符串最多包含 30 个字符。
int hu = 0;
void ReadWeight();
void compare(Huffman ps, int n1, int *s1, int *s2);
void Create(Huffman ps);
void code_read();
void compress();
void decompress(Huffman ps);
int main() {
ReadWeight();//权重
Huffman ps = (Huffman) malloc((2 * n+1) * sizeof(Node));//获取内存
Create(ps);//哈夫曼树
code_read();//把哈夫曼编码读取
compress();//压缩
decompress(ps);//解压
free(ps);
return 0;
}
// 读取权重文件
void ReadWeight() {
FILE *file1; // 权重文件
// 打开文件
file1 = fopen("权重.txt", "r");
if (file1 == NULL) {
printf("权重文件打开失败");
exit(1);
}
// 逐个读取文件内容并存储
char line[50];
while (fgets(line, sizeof(line), file1) != NULL) {
n++;//为了得到节点数
char *token = strtok(line, ":\n");
strcpy(leaf_name[n], token);//copy文字到数组
token = strtok(NULL, ":\n");
leaf_weight[n] = atoi(token);//把权重放数组
}
// 关闭文件
fclose(file1);
printf("权重文件读取成功\n");
}
// 选择两个最小的节点
void compare(Huffman ps, int n1, int *s1, int *s2) {
int min1 = 9999999, min2 = 9999999; // 初始化最小权重和次小权重
*s1 = 0; // 初始化最小权重节点的索引
*s2 = 0; // 初始化次小权重节点的索引
for (int i = 1; i <= n1; ++i) {
if (ps[i].parent == 0 && ps[i].weight < min1) { // 如果节点没有父节点且权重比最小权重小
*s2 = *s1; // 更新次小权重节点的索引
min2 = min1; // 更新次小权重
*s1 = i; // 更新最小权重节点的索引
min1 = ps[i].weight; // 更新最小权重
} else if (ps[i].parent == 0 && ps[i].weight < min2) { // 如果节点没有父节点且权重比次小权重小
*s2 = i; // 更新次小权重节点的索引
min2 = ps[i].weight; // 更新次小权重
}
}
}
// 创建哈夫曼树
void Create(Huffman ps) {
for (int i = 1; i <= 2 * n - 1; i++) {
ps[i].parent = ps[i].left = ps[i].right = 0;
if (i <= n) {
ps[i].weight = leaf_weight[i-1];
strcpy(ps[i].name, leaf_name[i-1]);
} else {
ps[i].weight = 0;
strcpy(ps[i].name, "\0");
}
}
for (int i = n + 1; i <= 2 * n - 1; ++i) {
int s1, s2;
compare(ps, i - 1, &s1, &s2);
ps[i].weight = ps[s1].weight + ps[s2].weight;//父节点权重
ps[s2].parent = ps[s1].parent = i;
ps[i].left = s1;
ps[i].right = s2;
}
printf("哈夫曼树创建成功\n");
}
// 读取哈夫曼编码
void code_read() {
// 打开文件
FILE *file2 = fopen("哈夫曼编码.txt", "r"); // 以只读方式打开文件
if (file2 == NULL) {
printf("打开文件失败\n"); // 如果文件打开失败,输出错误信息
exit(1); // 退出程序
}
// 读取内容
char line[50]; // 定义一个字符数组来存储每行的文本
while (fgets(line, sizeof(line), file2) != NULL) { // 逐行读取文件内容
line[strcspn(line, "\n")] = '\0'; // 去除换行符
char *token = strtok(line, ":"); // 使用冒号分割每行文本,获取第一部分
strcpy(huf[hu][0], token);//将第一部分存储在 huf 的第一列中
token = strtok(NULL, ":"); // 继续获取下一部分
strcpy(huf[hu++][1], token);//将第二部分存储在 huf 的第二列中,并递增huf的索引
}
// 关闭文件
fclose(file2); // 关闭文件
printf("哈夫曼编码文件读取成功\n"); // 输出成功读取文件的信息
}
// 压缩原文并得到二进制文件
void compress() {
// 打开文件
FILE *originalfile = fopen("三国演义.txt", "r");
if (originalfile == NULL) {
printf("原文打开失败");
exit(1);
}
FILE *file = fopen("压缩.txt", "w");
if (file == NULL) {
printf("压缩文件打开失败");
exit(1);
}
// 逐字压缩
char word[3]; // 用于存储从原始文件中读取的字符数组
unsigned char ch; // 用于存储压缩后的字符
int b = 0; // 用于跟踪当前字节中的位数
// 从原始文件中读取最多3个字符到word中
fgets(word, 3, originalfile);//(ANSI文件)
// 循环处理读取的字符
do {
int n1 = 1; // 辅助变量n1的初始化
for (int i = 0; i <= hu; i++) { // 遍历哈夫曼编码表
if (strcmp(huf[i][0], word) == 0) { // 如果找到与word对应的编码
for (int k = 0; k < strlen(huf[i][1]); k++) { // 对编码进行处理
ch += (huf[i][1][k] - '0'); // 按位将编码转换为字符
b++; // 记录处理位数
if (b == 8) { // 如果已经处理了一个完整的字节
fwrite(&ch, sizeof(char), 1, file); // 将字符写入文件
b = 0; // 重置位数
}
ch = ch << 1; // 左移一位
}
memset(word, '\0', sizeof(word)); // 清空word数组
}
n1++; // 增加辅助变量n1的值
}
if (n1 == 1) fprintf(file, "%s,", word); // 如果在哈夫曼编码表中找不到对应的编码,直接将原始字符写入文件
} while (fgets(word, 3, originalfile)); // 继续读取原始文件的内容并进行处理
// 关闭文件
fclose(originalfile);
fclose(file);
printf("压缩成功\n");
}
// 解压得到原文
void decompress(Huffman ps) {
// 打开文件
FILE *file = fopen("压缩.txt", "rb");
if (file == NULL) {
printf("压缩文件打开失败");
exit(1);
}
FILE *outfile = fopen("解压.txt", "w");
if (outfile == NULL) {
printf("解压文件打开失败");
exit(1);
}
// 逐字解压
int current = 2 * n - 1; // 从哈夫曼树的根节点开始
char bit;
while (fread(&bit, sizeof(char), 1, file) > 0) {
for (int i = 7; i >= 0; --i) {
if ((bit >> i) & 1) { // 读取一个比特
current = ps[current].right; // 如果为1,移动到右孩子
} else {
current = ps[current].left; // 如果为0,移动到左孩子
}
if (current <= n) { // 到达叶子节点,输出对应字符
fprintf(outfile, "%s", ps[current].name);
current = 2 * n - 1; // 从根节点重新开始
}
}
}
//关闭文件
fclose(file);
fclose(outfile);
printf("解压成功\n");
}
5 完结撒花
感谢观看