前言

一个简单的压缩软件,利用哈夫曼思想,构造哈夫曼编码,实现对文件的二进制压缩,以及解压,再利用MFC制作可视化操作界面,美化软件又简化文件操作。(各个步骤有解释可看)

软件主页面先看看

在这里插入图片描述

哈夫曼树结构

构造哈夫曼树存储结构:w权重即每个字节出现频度,byte结点数据即每个字节的ASCII码,fa双亲结点下标,le左孩子下标,ri右孩子下标,从下往上开始构建哈夫曼树。 根据已构造完成的哈夫曼树,从上往下开始构造每个结点的哈夫曼编码字符串,从根节点出发,如果下一个节点是其双亲的右孩子结点则在编码后接1,如果是左孩子结点则在编码后接0.存放哈夫曼树信息用到的是Huff_arr数组。

struct HaffNode {
    unsigned char byte;//为节点所代表的字符(ASCII码表对应的字符)
    long long w;//此节点代表字符的出现频度
    int num, fa, le, ri, code_len;//分别为节点在Huff_arr数组下标,双亲节点在Huff_arr数组下标,
    // 左子树下标,右子树下标,对应哈夫曼编码长度
    char code[256];//哈夫曼编码
    bool operator < (HaffNode x) const {
        return w > x.w;
    }
}Huff_arr[512]

步骤

①读取文件操作(包括初始化)

1、 读原文件,统计字节频度,定义Huffman树和Huffman编码的储存结构 读取文件,新建一个二进制文件用于存放统计数据,用while语句逐个读取源文件每一个字节,在每次读取的时候分别统计出现次数(权值)w和源文件长度file_length,直至文件结束。 2、 原文件字节频度统计 对字节频度排序,利用sort函数对Huff_arr[0]~ Huff_arr[520]的元素以weight为排序关键字进行降序排序。

void initpow(char *cp_inname) {
    unsigned char ch;
    CString str;
    FILE *ifp = fopen(cp_inname, "rb");
    if (ifp == NULL) {
        MessageBox(NULL, _T("该文件已存在,请重新输入"), _T("错误"), MB_ICONEXCLAMATION);
        return;
    }
    file_len = 0, bytes_cnt = 0;
    fread(&ch, 1, 1, ifp);
    while (!feof(ifp)) {
        Huff_arr[ch].w++, file_len++;
        fread(&ch, 1, 1, ifp);
    }
    fclose(ifp);
    for (int i = 0; i < 256; i++)
        if (Huff_arr[i].w > 0)
            bytes_cnt++;
    sort(Huff_arr, Huff_arr + 511);
}

②建树

利用优先队列(priority_queue QUEUE;),每次取队头元素(权值第一小)First节点,出队后再取队头元素S(权值第二小)econd,然后将两棵子树合并为一棵子树,权值相加,并将新子树的根节点顺序存放到数组huff_arr ,再把新子树的根节点放进队列再次循环步骤,直到队列的个数为1为止。

void createhafftree() {
    priority_queue<HaffNode> QUEUE;
    HaffNode First, Second, Sum;
    int tot = bytes_cnt;
    while (QUEUE.size())QUEUE.pop();
    for (int i = 0; i < bytes_cnt; i++)
        Huff_arr[i].num = i, QUEUE.push(Huff_arr[i]);
    while (QUEUE.size() > 1) {
        First = QUEUE.top(), QUEUE.pop();
        Second = QUEUE.top(), QUEUE.pop();
        Sum.num = tot, Sum.w = First.w + Second.w;
        Sum.fa = -1, Sum.le = First.num, Sum.ri = Second.num;
        strcpy(Sum.code, "");
        Huff_arr[First.num].fa = Sum.num, Huff_arr[Second.num].fa = Sum.num;
        Huff_arr[tot++] = Sum;
        QUEUE.push(Sum);
    }

③构造哈夫曼编码

根据已构造完成的哈夫曼树,从上往下开始构造每个结点的哈夫曼编码字符串,从根节点出发,如果下一个节点是其双亲的右孩子结点则在编码后接1,如果是左孩子结点则在编码后接0.哈夫曼编码树的左分支代表 0,右分支代表 1,则从根结点到每个叶子结点所经过的路径组成的 0 和 1 的序列便成为该叶子结点对应字符的编码。

void createhaffcode() {
    int tot = bytes_cnt * 2 - 1;
    Huff_arr[tot - 1].code[0] = '\0';
    for (int i = tot - 2; i >= 0; i--) {
        strcpy(Huff_arr[i].code, Huff_arr[Huff_arr[i].fa].code);
        if (Huff_arr[Huff_arr[i].fa].ri == i)
            strcat(Huff_arr[i].code, "1");
        else
            strcat(Huff_arr[i].code, "0");
        Huff_arr[i].code_len = strlen(Huff_arr[i].code);
    }
}

④生成压缩文件

生成压缩码:先找出根节点的位置,然后从根节点一直往下进行编码,根据左孩子置为0,右孩子置为1这个规则一直往下编码,根据编码继承,可以直接在父节点的编码后面置0或1即可。 根据编码写入文件:得到哈夫曼编码后先将缓冲区置为空,然后按照每8位为一个字节,将二进制转为十进制进行写入文件,如果最后缓冲区还有元素,则在后面补8个0,然后再整除8,变为8位元素。

ofp = fopen(cp_outname, "wb");
    if (ofp == NULL) {
        MessageBox(NULL, _T("未能成功打开文件"), _T("错误"), MB_ICONEXCLAMATION);
        return;
    }
    fprintf(ofp, "%d,%s,%lld,%d,", strlen(file_extension), file_extension, file_len, bytes_cnt);
    for (int i = 0; i < bytes_cnt; i++)
        fprintf(ofp, "%c,%lld,", Huff_arr[i].byte, Huff_arr[i].w);
    ifp = fopen(cp_inname, "rb");
    if (ifp == NULL) {
        MessageBox(NULL, _T("打开文件失败"), _T("错误"), MB_ICONEXCLAMATION);
        return;
    }
    strcpy(buff, "");
    ch = fgetc(ifp);
    while (!feof(ifp)) {
        if (Buffmax - strlen(buff) > 256) {
            for (int i = 0; i < bytes_cnt; i++) {
                if (Huff_arr[i].byte == ch) {
                    strcat(buff, Huff_arr[i].code), ch = fgetc(ifp);
                    break;
                }
            }
        }
        else {
            flushBuffer(ofp);
        }
    }
    flushBuffer(ofp);
    if (strlen(buff) > 0) {
        strcat(buff, "00000000");
        flushBuffer(ofp);
        strcpy(buff, "");
    }
    fclose(ofp);
    fclose(ifp);

注意:

生成压缩文件一定要在文件里面记录相应的扩展名以及哈夫曼树的重要存储结构,即源文件对应的字符和字符频度,在将哈夫曼编码每八位转成一个十进制值对应的字符时,有可能哈夫曼编码不是8的整数倍,需要在哈夫曼编码最后面补充8个0,多余的哈夫曼编码便可借0补位,以此避免二进制文件写入错误。

为了读文件快点,利用缓冲区

void flushBuffer(FILE * fp) { // 把缓冲区中,尽可能多的字节,写入文件中 
    strcpy(bufstr, "");
    unsigned char temp = 0;
    int byte_data_num = strlen(buff) / 8, i;
    for (i = 0; i < byte_data_num; i++) {
        temp = 0;
        for (int j = 0; j < 8; j++) {
            if (buff[i * 8 + j] == '1')
                temp += pow(2, 7 - j);
        }
        bufstr[i] = temp;
    }
    bufstr[i] = '\0';
    fwrite(bufstr, 1, byte_data_num, fp);
    strcpy(buff, buff + byte_data_num * 8);
}

⑤解压文件

1、 读压缩文件的头部 (1) 读源文件的扩展名长度,把扩展名存储以便生成解压文件可用来定义文件类型,读入源文件的总字节数,读入源文件中被编码的字节总数 (2) 根据(1)中读入的被编码的字节总数,依此读取字符和字符频度,初始化哈夫曼树存储结构,构造哈夫曼树 (3) 读取哈夫曼总编码生成的二进制数据。 2、 对压缩文件进行解压 (1) 读取分哈夫曼总编码生成的二进制数据分批次装满缓冲区,写入文件 (2) 缓冲区内的下一位,若是0,则转向左孩子,若是1,则转向右孩子 (3) 找出叶子节点,并把该字节写入解压文件中,即是还原每个节点对应的哈夫曼编码,找出每个哈夫曼编码对应的节点,将节点对应的ASCII码的字符写入生成文件

ifp = fopen(dcp_inname, "rb");
    if (ifp == NULL) {
        MessageBox(NULL, _T("此压缩文件不存在或被占用!"), _T("错误"), MB_ICONEXCLAMATION); 
        return;
    }
    strcpy(dat_file_extension, "");
    fscanf(ifp, "%d,", &sufname_len);
    fread(&dat_file_extension, sufname_len, 1, ifp);
    fscanf(ifp, ",%lld,%d,", &file_len, &bytes_cnt);
    for (int i = 0; i < bytes_cnt; i++)
        fscanf(ifp, "%c,%lld,", &Huff_arr[i].byte, &Huff_arr[i].w);

    //构造哈弗曼树并输出
    createhafftree();
    /*printhafftree();*/

    //生成文件绝对路径
    strcat(dcp_outname, ".");
    strcat(dcp_outname, dat_file_extension);
    //解压
    ofp = fopen(dcp_outname, "wb");
    if (ofp == NULL) {
        MessageBox(NULL, _T("解压文件生成失败!"), _T("错误"), MB_ICONEXCLAMATION);
        return;
    }
    strcpy(buff, ""), strcpy(block, "");
    fread(buff, 1, Buffmax - 1, ifp);
    root = bytes_cnt * 2 - 2, trcur = root;
    while (dfile_len < file_len) {
        if (blcur >= Blockmax - 1) {
            fwrite(block, 1, blcur, ofp);
            blcur = 0;
        }
        if (Huff_arr[trcur].le == -1) {
            block[blcur++] = Huff_arr[trcur].byte, block[blcur] = '\0';
            trcur = root, dfile_len++;
            if (blcur == 510) {
                int xxdx = 1;
                xxdx++;
            }
        }
        else {
            if ((buff[bucur] >> (7 - bycur)) & 1)
                trcur = Huff_arr[trcur].ri;
            else
                trcur = Huff_arr[trcur].le;
            if (bycur < 7)
                bycur++;
            else {
                bycur = 0, bucur++;
                if (bucur >= Buffmax - 1)
                    fread(buff, 1, Buffmax - 1, ifp), bucur = 0;
            }
        }
    }
    fwrite(block, 1, blcur, ofp);
    fclose(ifp), fclose(ofp);

MFC主要三个按钮响应事件代码

void CHuffmanDlg::OnBnClickedButton1() //压缩文件按钮对应的事件
{
    // TODO: 在此添加控件通知处理程序代码
    CString strFile = _T("");
    CString str3;
    str3.Format(_T("请选择所需要进行压缩的文件:"));
    if (MessageBox(str3, _T("提示"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
        return;
    }
    else {
        CFileDialog    dlgFile(TRUE, NULL, NULL, OFN_HIDEREADONLY, _T("Describe Files All Files (*.*)|*.*||"), NULL);

        if (dlgFile.DoModal())
        {
            strFile = dlgFile.GetPathName();
        }
    }
    if (strFile == "")
        return;
    CString str4;
    Edit_text YS;
    str4.Format(_T("请选择是否为生成文件重新命名:"));
    named_ok = false;
    //是否进行对生成文件的命名
    if (MessageBox(str4, _T("选择"), MB_ICONQUESTION | MB_YESNO) == IDNO) {
        named_ok = false;
    }
    else {
        named_ok = true;
        YS.DoModal();
    }
    //对文件名进行修改
    USES_CONVERSION;
    char * inFileName = T2A(strFile);
    int ok = 0;
    int pos1 = 0,pos2;
    strcpy(cp_file_name, "");
    strcpy(file_extension, "");
    char temp[MAX_PATH] = "";
    for (int i = strlen(inFileName) - 1; i >= 0; i--) {
        if (inFileName[i] == '\\') {
            break;
        }
        if (ok == 1) {
            temp[pos1++] = inFileName[i];
        }
        if (inFileName[i] == '.') {
            pos2 = i + 1;
            ok = 1;
        }
    }
    int tot = 0;
    for (int i = pos2; i < strlen(inFileName); i++)
        file_extension[tot++] = inFileName[i];
    int num = 0;
    for (int i = pos1 - 1; i >= 0; i--) {
        cp_file_name[num++] = temp[i];
    }
    strcat(cp_file_name, ".dat");
    CString NAME= YS.FILE_NAME + ".dat";//文本框传来的信息
    if (!named_ok)
        NAME= CA2CT(cp_file_name);
    char szPath[MAX_PATH];//存放选择的目录路径
    CString str1, str2, FileName;
    CTime m_time;
    ZeroMemory(szPath, sizeof(szPath));
    BROWSEINFO bi;
    bi.hwndOwner = m_hWnd;
    bi.pidlRoot = NULL;
    bi.pszDisplayName = (LPWSTR)szPath;
    bi.lpszTitle = _T("请选择生成文件的目录:");
    bi.ulFlags = BIF_BROWSEINCLUDEFILES | BIF_NEWDIALOGSTYLE;
    bi.lpfn = NULL;
    bi.lParam = 0;
    bi.iImage = 0;
    LPITEMIDLIST lp = SHBrowseForFolder(&bi);
    FileName = m_time.Format(NAME); 
    SHGetPathFromIDList(lp, (LPWSTR)szPath);
    str2.Format(_T("%s"), szPath);
    CString filePath = str2 + "\\" + FileName;//路径+文件名
    if (lp && SHGetPathFromIDList(lp, (LPWSTR)szPath))
    {
        str1.Format(_T("选择生成文件的路径为: %s"), szPath);
        if (MessageBox(str1, _T("路径"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
            return;
        }
        else {
            USES_CONVERSION;
            //函数T2A和W2A均支持ATL和MFC中的字符
            char * outFileName = T2A(filePath);
            clock_t  clockBegin, clockEnd;
            clockBegin = clock();
            CFile cfile;
            DOUBLE size1, size2;
            if (cfile.Open(strFile, CFile::modeRead))
            {
                size1 = cfile.GetLength();
            }
            cfile.Close();
            compressFile(inFileName, outFileName);
            clockEnd = clock();
            DOUBLE TIME = (clockEnd - clockBegin)/( CLOCKS_PER_SEC);
            if (cfile.Open(filePath, CFile::modeRead))
            {
                size2 = cfile.GetLength();
            }
            cfile.Close();  
            UpdateData(FALSE);
            CString TIMESTR;
            size1 /= 1024;
            size2 /= 1024;
            DOUBLE YSL = size2/ size1 * 100;
            char s = '%';
            TIMESTR.Format(_T("压缩文件耗时为:%.2lfs\n起始文件大小为:%.2lfKB\n压缩文件大小为:%.2lfKB\n文件的压缩率为:%.2lf%c"), TIME,size1, size2,YSL,s);
            MessageBox(TIMESTR, _T("压缩成功"));
        }
    }
    else
    {
        AfxMessageBox(_T("无效的目录,请重新选择"));
        return;
    }
}
void CHuffmanDlg::OnBnClickedButton2()//解压文件按钮对应的事件
{
    // TODO: 在此添加控件通知处理程序代码
    CString strFile = _T("");
    CString str3;
    str3.Format(_T("请选择所需要进行解压的文件:"));
    if (MessageBox(str3, _T("提示"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
        return;
    }
    else {
        CFileDialog    dlgFile(TRUE, NULL, NULL, OFN_HIDEREADONLY, _T("Describe Files All Files (*.*)|*.*||"), NULL);

        if (dlgFile.DoModal())
        {
            strFile = dlgFile.GetPathName();
        }
    }
    if (strFile == "")
        return;
    CString str4;
    JIEYA_FILENAME JY;
    str4.Format(_T("请选择是否为生成文件重新命名:"));
    named_ok = false;
    if (MessageBox(str4, _T("选择"), MB_ICONQUESTION | MB_YESNO) == IDNO) {
        named_ok = false;
    }
    else {
        named_ok = true;
        JY.DoModal();
    }
    //对文件名进行修改
    USES_CONVERSION;
    char * inFileName = T2A(strFile);
    int ok = 0;
    int pos = 0;
    strcpy(dcp_file_name, "");
    char temp[MAX_PATH] = "";
    for (int i = strlen(inFileName) - 1; i >= 0; i--) {
        if (inFileName[i] == '\\') {
            break;
        }
        if (ok == 1) {
            temp[pos++] = inFileName[i];
        }
        if (inFileName[i] == '.') {
            ok = 1;
        }
    }
    int num = 0;
    for (int i = pos - 1; i >= 0; i--) {
        dcp_file_name[num++] = temp[i];
    }
    CString NAME = JY.TEXT_NAME;//文本框传来的信息
    if (!named_ok)
        NAME = CA2CT(dcp_file_name);
    char szPath[MAX_PATH];//存放选择的目录路径
    CString str1, str2, FileName;
    CTime m_time;
    ZeroMemory(szPath, sizeof(szPath));
    BROWSEINFO bi;
    bi.hwndOwner = m_hWnd;
    bi.pidlRoot = NULL;
    bi.pszDisplayName = (LPWSTR)szPath;
    bi.lpszTitle = _T("请选择生成文件的目录:");
    bi.ulFlags = BIF_BROWSEINCLUDEFILES | BIF_NEWDIALOGSTYLE;
    bi.lpfn = NULL;
    bi.lParam = 0;
    bi.iImage = 0;
    LPITEMIDLIST lp = SHBrowseForFolder(&bi);
    FileName = m_time.Format(NAME); 
    SHGetPathFromIDList(lp, (LPWSTR)szPath);
    str2.Format(_T("%s"), szPath);
    CString filePath = str2 + "\\" + FileName;//路径+文件名无扩展名
    if (lp && SHGetPathFromIDList(lp, (LPWSTR)szPath))
    {
        str1.Format(_T("选择生成文件的路径为: %s"), szPath);
        if (MessageBox(str1, _T("路径"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
            return;
        }
        else {
            USES_CONVERSION;
            char * outFileName = T2A(filePath);
            clock_t  clock1, clock2;
            clock1 = clock();
            deCompressFile(inFileName, outFileName);
            clock2 = clock();
            DOUBLE TIME = (clock2 - clock1)/ (CLOCKS_PER_SEC);
            CString TIMESTR;
            UpdateData(FALSE);
            TIMESTR.Format(_T("\t解压成功!!!\t\n\t解压文件耗时为:%.2lfs\t"), TIME);
            MessageBox(TIMESTR, _T("信息提示"));
        }
    }
    else
    {
        AfxMessageBox(_T("无效的目录,请重新选择"));
        return;
    }
}
void CHuffmanDlg::OnBnClickedButton3()//退出按钮对应的事件
{
    // TODO: 在此添加控件通知处理程序代码
    CString str3;
    str3.Format(_T("是否确定要退出程序?"));
    if (MessageBox(str3, _T("提醒"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
        return;
    }
    else {
        PostQuitMessage(0);
    }
}

最后操作界面演示

启动界面

启动界面

①压缩文件

(1)选择所要压缩的文件 2 (2)可为生成的压缩文件命名,并选择生成文件目录 3 4 (3)压缩完成后,会显示:压缩耗时,起始文件大小,压缩文件大小,文件压缩率。 在这里插入图片描述

②解压文件

(1)选择所要解缩的文件 6 (2)可为生成的解压文件命名,并选择生成文件目录 7 9 (3)解压完成后,会显示解压时间

8

最后的最后完整包

由于利用了MFC 有一堆MFC的头文件和源文件,所以就不可能把所有代码贴上来了,制作了一个完整的压缩文件包可以供下载。 在这里插入图片描述 图中①是exe文件打开就可以运行,图中②是程序所有的文件,③可以用VS打开来,里面就是主要的代码。 打开后,主要的核心代码在这个CPP里面 在这里插入图片描述

最后放上 完整文件包完整文件包ZIP下载地址

评论