哈夫曼树、哈弗曼编码:压缩,解压文本文件

哈夫曼树、哈弗曼编码:压缩,解压文本文件

  • 由文本文件生成哈夫曼编码,即:压缩
    *思路、步骤
    • 性能分析
  • 代码
    *需注意的的问题
    *不足与优化
  • 译码,即:解压
    *思路步骤
    *代码
    *需注意的问题

由文本文件生成哈夫曼编码

思路、步骤:

1.读入文本文件的内容,并按照每一个不同字符出现的频率赋予相应的权重。这里因为我需要读入汉字(注意汉字及汉语标点符号在计算机中的存储都是两位字符类型大小),所以这里我统计每个汉字或标点出现的次数的方法是建立一个结构体数组(设不同的汉字由NUM种,宏定义可手动修改,当然也可改为自动添加),每读入一个新的汉字就从结构体数组的开头开始检测是否出现过,出现过则会在检测到结尾之前匹配成功,该汉字数目加一,不成功则在结束检测时把末尾结构体元素设为此汉字,以此类推直到读入到文件末尾。

**一定要注意这里是读取汉字时的情况!!**若读英文字母文本则简单的多,因为英文所有字符的ascii码分布都在0 - 255 之间,所以我们只需要建立一个大小为256的数组即可以英文字符的ascii码做下标存储并记录字符出现次数,这样会大大减小时间复杂度!!

性能分析:
  1. 读汉字时,新读入的一个汉字必须经过和之前已经存在的汉字一个一个的比较才能确定是否出现过。那么在最坏情况下,若文本文件中的n个汉字均各不相同,那么其时间复杂度将会达到O(n^2)的大小,当文件大小过大时这是无法想象的。(如果你有时间、空间复杂度更小的方法欢迎在评论区里提出~
  2. 读英文文本时的情况要好得多,因为字符的ascii码值唯一,所以能以O(1)的时间复杂度确定字母是否出现并累加其出现次数。

代码

下面上代码,**亲自测试无误,源代码无删改,请放心观看使用!!

//----------------------------------------------------------------------
//作者:@你隔壁的小傻子
//更新时间:2019 - 12 - 21
//多有不足,感谢支持
//----------------------------------------------------------------------
/*思路:
赫夫曼树、前缀码
-读入txt文件
--判断有多少个不同字符,按比例赋权值
-构造赫夫曼树
-生成前缀码
-前缀码存储到文件中  //完成第一步
-译码
--存储到文件中
*/
#include<iostream>
#include<stdlib.h>
#include<windows.h>
#include<vector>
#include<conio.h>
#include<string.h>
#include<map>
#include<math.h>
#include<fstream>
using namespace std;

#define out_file "d:huffman_code.txt"//存放二进制数
#define in_file "d:source.txt"    //原文件
#define translate_file "d:translate.txt"       //存放译码后信息,可与原文件比较
#define NUM 101       //假定文件中最多有多少种不同汉字,可改为越界自动添加
//定义每个汉字的属性结构体
typedef struct character
{
    char a[3]= {0};//保存的汉字
    int num = 0;//该汉字的数目
    float weight;//该汉字权重
} charac;
//节点结构体
typedef struct Huffman_Hashtable
{
    double weight;//权
    unsigned int parent;
    unsigned int lchild;
    unsigned int rchild;
    bool flag_ = false; //在选择最小权重节点是作为标记
} Huf_Hashtable, *huftre;

//--------------------------------------------编码并存储-----------------------------------//

//从txt文件中读入数据到data[]中
//这里因为是汉字所以采用了这种做法,
//如果是英文字母可以采用创建一个256
//大小的字符数组并将字符以他们的ascii
//值作为下标存储在数组中,这样在文本
//数据很多时能大大优化时间复杂度
int read_txt(charac *data)
{
    int num = 1;//指最远位置上的汉字的下一位
    ifstream in(in_file);
    if(!in.is_open())        //处理异常
    {
        cout << "cannot open file";
        system("pause");
    }
    in.seekg(0, ios::beg);
    while(in.peek()!=EOF)//将文件中内容读到结构体数组中
    {
        in.read(data[0].a, 2 * sizeof(char));
        for (int z = 1; z <= num; ++z)
        {
            if(strcmp(data[0].a,data[z].a) == 0)//已存在相同汉字
            {
                ++data[z].num;
                break;
            }
            else
            {
                if(z == num)//将新汉字添加到末尾
                {
                    strcpy(data[num].a, data[0].a);
                    ++data[num].num;
                    ++num;
                    break;
                }
                else
                {
                    continue;
                }
            } 
        }
    }

    in.close();
    return num;//返回不同的汉字数
}

//给不同汉字赋权重
void distr_weight(charac data[], int num)
{
    //计算所有字数之和
    int n;//控制循环
    float sum(0);//所有字数之和
    for (n = 1; n <= num; ++n)
    {
        sum += data[n].num;
    }
    for (n = 1; n <= num; ++n)
    {
        data[n].weight = data[n].num / sum;
    }
}

//在data【1】到data【num - 1】间选择权重最小的两个节点分别为min_1,min_2
void Select(huftre &HT, int n, int &min_1, int &min_2, int m)
{
    
    //找到最小的两个权重的下标
    int mark_1,
        mark_2;
    min_1 = 1;
    min_2 = 1;

    //先求最小
    for (mark_1 = 1; mark_1 <= n; ++mark_1)
    {
        if(HT[mark_1].flag_ == true)
            ++min_1;
        else
            break; 
    }
    for (mark_2 = 1; mark_2 <= n; ++mark_2)
        {
            if (HT[min_1].weight > HT[mark_2].weight && HT[mark_2].flag_ == false)
            {
                min_1 = mark_2;
            }
        }
    
    HT[min_1].flag_ = true;

    //求次小
    for (mark_1 = 1; mark_1 <= n; ++mark_1)
    {
        if(HT[mark_1].flag_ == true)
            ++min_2;
        else
            break;
    }
    for (mark_2 = 1; mark_2 <= n; ++mark_2)
        {
            if(HT[min_2].weight > HT[mark_2].weight && HT[mark_2].flag_ == false)
            {
                min_2 = mark_2;
            }
        }

    HT[min_2].flag_ = true;
}

//构造赫夫曼树
char **huffmantree(charac *data, int num, huftre &HT)
{
    const int m = 2 * num - 1;//huffman树的节点个数
    int z;//控制循环
    huftre p = HT;


    //初始化数组前num个元素
    for (z = 1, p = HT + 1; z <= num; ++z,++p)
    {
        *p = {data[z].weight, 0, 0, 0};
    }

    //初始化数组num之后的元素
    for (; z <= m; ++z, ++p)
    {
        *p = {0, 0, 0, 0};
    }


    //选出权值最小的两个结构体,用他们的内容建树
    int min1, min2;//权值最小的两个数据的下标
    for (z = num + 1; z <= m; ++z)//构建赫夫曼树
    {
        Select(HT, z - 1, min1, min2, m);
        HT[min1].parent = z;
        HT[min2].parent = z;
        HT[z].lchild = min1;
        HT[z].rchild = min2;
        HT[z].weight = HT[min1].weight + HT[min2].weight;
    }



    /*--------------叶子到根逆向求每个字的赫夫曼编码----------*/
    int i;
    int c;//参与循环
    int start;
    char **HC = (char **)malloc(sizeof(char *) * (num + 1));//存储每一位节点代表的赫夫曼编码,0号不用


    char null[] = "";//消除警告
    HC[0] = null;


    char *cd = (char *)malloc(sizeof(char) * num);
    cd[num - 1] = '\0';//结束符
    for (z = 1; z <= num; ++z)//求编码
    {
        start = num - 1;
        for (c = z, i = HT[z].parent; i != 0; c = i, i = HT[i].parent)
        {
            if(HT[i].lchild == c)
                cd[--start] = '0';
            else
                cd[--start] = '1';
        }
        HC[z] = (char *)malloc(sizeof(char) * (num - start));


        strcpy(HC[z], &cd[start]);//从第start位开始复制

    }
    
    
    free(cd);//释放空间
    return HC;
}

//压缩
void write_data(char **HC, const charac *data, int num)
{
    ofstream out(out_file, ios::trunc | ios::binary);//注意这里是每次打开都清除原文件的数据
    ifstream in(in_file);

    //逐字压缩到新文件中
    char letter[3] = {'\0'};
    int z;//控制循环
    while(in.peek() != EOF)
    {
        in.read(letter, sizeof(char) * 2);
        for (z = 1; z <= num; ++z)
        {
            if(strcmp(letter, data[z].a) == 0)
            {
                out << HC[z];//写入对应赫夫曼编码
                break;
            }
        }
    }

    in.close();
    out.close();
}
//--------------------------------------------------end--------------------------------------------//

需要注意的一些问题

  1. 在读文件时判断文件指针是否已经到末尾,使用
while(!in.eof()) {...}

会出现问题!那就是在读取内容时文件指针到最后一个字符时并不会触发eof,再读一次读不到数据才触发eof,这样就会导致最后的内容被重复读取输出。
解决方法

while(in.peek() != NULL)

用peek函数就能完美解决这个问题。

  1. 通过构造哈夫曼树得到哈夫曼编码的算法一定要清楚地知道、运用

不足与优化

  1. 要通过哈夫曼编码真正做到压缩文件,需要用到位操作,即:把八个二进制数整合到一个字节当中,然后通过整数形式存储到目标文件,这样才能最终达到压缩文件大小的目的!当然你还需要存储的是每一个不同的字符到底是什么还有它们分别的权重,这样在后续译码过程中才能正常进行。(这里因为我现在并没有必要真正做出解码效果,并且学习位操作现在并非必要,所以就先省略。解码部分也是,只写出了从哈夫曼编码译码的过程…真的 不是我懒啊哈哈哈)

译码

思路、步骤:

  1. 先读入目标文件中的,初始化信息,即:每个字符以及其所占权重(完整版解码压缩需用位操作,需要的朋友可去单独学习)。建立哈夫曼树,利用循环从哈夫曼树根开始判断,是0则走左子树,1则走右子树,直到左右孩子指针均为null,则输出与之对应的字符。以此类推即可译码。

代码

译码部分的代码如下,

//-----------------------------------------------------译码-----------------------------//
void translate_code(const huftre &HT, const charac *data, int num)
{
    ifstream in(out_file);
    ofstream out(translate_file, ios::trunc);

    int head_node;
    in.seekg(0, ios::beg);//将指针移动到开头
    while(in.peek() != EOF)
    {
        for (head_node = 2 * num - 1; HT[head_node].lchild != 0 || HT[head_node].rchild != 0;)
        {
            if(in.get() - '0' == 0)
                head_node = HT[head_node].lchild;
            else
                head_node = HT[head_node].rchild;
        }
        out.write(data[head_node].a, sizeof(char) * 2);
    }

    in.close();
    out.close();
}

//-----------------------------------------------------end--------------------------------//

需要注意的问题

  1. 译码一定要采用首先根据权重信息建立哈夫曼树再译码这一方法,我一开始的想法也是读入一位哈夫曼编码就在所有编码中匹配一次,最终找到对应的字符。这样的效率以及时间复杂度在实际应用中是无法胜任的。一定要注意这一点。

  2. 最后。代码还差了主函数,加上:

int main()
{
    int num;//不同的汉字数
    charac data[NUM];//data[0]为哨兵
    num = read_txt(data) - 1;//从txt文件中读入数据到data[]中
    distr_weight(data, num);           //赋权重
    //====================================测试===============================
    #if 0
    for (int v = 1; v <= num; ++v)
    {
        cout << data[v].a << ": " << data[v].weight << " and " << data[v].num << endl;
    }
    system("pause");
    #endif
    //======================================================================
    huftre HT = (huftre)malloc(sizeof(Huf_Hashtable) * 2 * num); //0号单元不用
    char **HC = huffmantree(data, num, HT);//构造赫夫曼树
    write_data(HC, data, num);
    translate_code(HT, data, num);
    system("pause");
    return 0;
}

结束语

哈夫曼(书上也叫赫夫曼)总的来说,可能刚开始上手困难一点(也就是核心一点)的地方就在利用循环得到每一个字符的哈夫曼编码这里,当你真正写一遍再回过头来看也并没有刚上手那么毫无头绪,反而心如明镜,一片通透,所以一定要上手写。(之前也提到了这里因为性价比关系,对我个人而言还没有必要花费过多时间去学习位操作相关知识搞一个完整的压缩解压过程,因为最近时间真的非常紧张!寒假有时间可能会去深入了解学习,现在就先暂且放下!)希望能够对看到本文的人有所帮助!有建议评论区见~~

注:下一篇博客会是哈希表的相关内容,建表,处理冲突,限制查找长度以及算法分析

  • 4
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值