[数据结构与算法综合实验]二叉树与哈夫曼图片压缩

Spring-_-Bear 的 CSDN 博客导航

一、实验目标

  1. 掌握树的存储结构。
  2. 掌握二叉树的三种遍历方法。
  3. 掌握 Huffman 树、Huffman 编码等知识和应用。
  4. 使用 C++、文件操作和 Huffman 算法实现 “图片压缩程序” 专题编程。

二、实验任务

压缩软件是利用特定算法来压缩数据的工具,压缩后生成的文件称为压缩包(archive)。如果想使用其中的数据,就得用压缩软件对数据进行解压。利用压缩软件对文件中重复的数据进行压缩,可以减小文件中的字节总数,使文件能够通过互联网连接实现更快传输,此外还可以减少文件的磁盘占用空间。常用的压缩格式有 rar、zip 等。

压缩可以分为无损压缩与有损压缩两种。无损压缩后的文件,经过解压能够完全恢复原始数据;有损压缩的文件则无法完全恢复。rar、zip 等格式都是无损压缩格式。音乐文件格式 mp3、图片文件格式 jpg 都是有损压缩格式。

计算机文件是由一个个字节组成的,1 个字节有 0~255 共 256 种可能的值,每个字节的编码长度都是 8 位。由于文件中的字节总是会重复出现,可以对不同的字节设计长度不等的编码,让出现次数较多的字节,采用尽可能短的编码,那么文件编码的总长便可减少。

统计文件中 256 种不同的字节重复的次数,以每种字节重复的次数作为权值(weight)构造一棵有 256 个叶子节点的二叉树。若带权路径长度达到最小,称这样的二叉树为最优二叉树,即 Huffman 树(Huffman tree)。

Huffman 树从根到每个叶子都有一条路径。对路径上的各分支,约定指向左子树根的分支编码为 “0”,指向右子树根的分支编码为 “1”。从根到每个叶子相应路径上的 “0” 和 “1” 组成的序列,就是这个叶子节点的编码,称为 Huffman 编码。

功能要求:使用 Huffman 压缩算法,对一幅 BMP 格式的图片文件进行压缩。图片文件名为 “Pic.bmp”,内容如下图所示。压缩后保存为 “Pic.bmp.huf” 文件。

在这里插入图片描述

使用 Visual Studio 2019 作为开发工具,开发一个控制台程序,使用 Huffman 压缩算法对图片文件 “Pic.bmp” 进行压缩,具体要求如下:

  1. 创建工程:创建 HuffmanCompress 工程,定义入口函数 int main()。
  2. 读取原文件:读取文件,统计 256 中字节重复的次数。
  3. 生成 Huffman 树:根据上一步的统计结果,构建 Huffman 树。
  4. 生成 Huffman 编码:遍历 Huffman 树,记录 256 个叶子节点的路径,生成 Huffman 编码。
  5. 压缩编码:使用 Huffman 编码,对原文件中的字节重新编码,获得压缩后的文件数据。
  6. 保存文件:将编码过的数据,保存到文件 “Pic.bmp.huf” 中。

三、分析和设计

通过需求可知,本程序首先需要通过读取文件,生成一棵带权二叉树。树在程序中可使用链式结构、顺序结构两种方式来表示。已知这棵树的叶子节点数为 256,其存储空间是固定的,因此,本程序将使用顺序结构来表示这棵二叉树。

5.3.1 编码构思

  1. 定义存储结构:定义一个结构体 HTNode 来表示二叉树的叶子节点,记录每个节点的权值、父节点、左孩子和右孩子。创建结构体数组 HTNode aHuffmanTree[512] 来存储这棵带权二叉树。
  2. 遍历算法:其次,生成 Huffman 树后,要遍历这棵二叉树,根据叶子节点的路径来生成 Huffman 编码表。遍历二叉树有先序遍历、中序遍历、后序遍历三种方式。本程序将使用先序遍历的方式,查找二叉树上所有的叶子节点,生成对应的 Huffman 编码。定义二维字符数组 char[256][] 来存储所有叶子节点的 Huffman 编码,供编码时使用。
  3. 压缩编码:由于 Huffman 编码表是以字符数组的形式保存的,重新编码后的数据将是一个很长的字符串。定义 Str2byte 函数,将形如 “01010101” 字符串转换成字节,才能得到最终的编码。将其保存到 “*.huf” 中,即实现了文件压缩。
  4. 解压缩:最后,为了保证压缩后的数据能够被正确解压,除了保存压缩的数据之外,还应保存原文件的长度及 256 种字节的重复次数。定义一个文件头,用于保存这些信息。在保存压缩文件时,同时向文件中写入文件头和压缩数据,以保证文件能够被还原。

5.3.2 程序设计

  1. 创建 Main.cpp 文件,定义 int main(void) 函数,作为程序的入口函数。
  2. 创建 Huffman.h 文件与 Huffman.cpp 文件,实现 Huffman 算法相关的函数。
    • 定义 HTNode 结构体。
    • 定义 HuffmanTree 函数,用于生成 Huffman 树。
    • 定义 HuffmanCode 函数,用于生成 Huffman 编码。
  3. 创建 Compress.h 文件与 Compress.cpp 文件,实现文件的压缩算法。Compress.cpp 文件中将包含 Huffman.h 文件,使用其中的 Huffman 算法相关的函数。
    • 定义 Compress 函数,用于实现文件压缩。
    • 定义 InitHead 函数,读取原文件,初始化文件头数据信息。
    • 定义 Encode 函数,利用 Huffman 编码实现文件压缩。
    • 定义 Str2byte 函数,用于将“01010101”形式的字符串字节。
    • 定义 WriteFile 函数,用于将压缩后的数据写入新文件。

5.3.3. 业务流程

使用 Huffman 算法压缩图片,业务流程如下:

在这里插入图片描述

四、运行示例

在这里插入图片描述

五、程序源码

5.1 main.cpp

#include <iostream>

#include "Compress.h"

using namespace std;

int main(void)
{

    cout << "========== Huffman 文件压缩 ==========" << endl;

    cout << "请输入文件名:";
    char filename[256];
    cin >> filename;

    if (Compress(filename) == 1)
        cout << "\n文件压缩成功!" << endl;
    else
        cout << "\n文件压缩失败!" << endl;

    return 0;
}

5.2 global.h

#pragma once

#define OK 1
#define ERROR 0

5.3 Compress.h

#pragma once

// Encode 函数声明会用到 HuffmanCode
#include "Huffman.h"

// 文件头
struct HEAD
{
    char type[4];
    int length;
    int weight[256];
};

// 实现文件压缩
int Compress(const char *pFilename);

// 读取源文件和初始化头文件的信息
int InitHead(const char *pFilname, HEAD &sHead);

// 利用 Huffman 编码实现压缩编码
int Encode(const char *pFilname, const HuffmanCode pHC, char *pBuffer, const int nSize);

// 将二进制字符串转换成字节
char Str2byte(const char *pBinStr);

// 生成压缩文件
int WriteFile(const char *pFilename, const HEAD sHead, const char *pBuffer, const int nSize);

5.4 Compress.cpp

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <stdlib.h>
#include <string.h>

#include "Compress.h"
#include "Huffman.h"
#include "global.h"

using namespace std;

const int SIZE = 256;

// 实现文件压缩
int Compress(const char *pFilename)
{
    cout << endl;
    // 打开并扫描文件
    cout << "正在读取文件……" << endl
         << endl;
    int weight[256] = {0};
    FILE *in = fopen(pFilename, "rb");

    int tempch;
    // 获取权重
    while ((tempch = getc(in)) != EOF)
        weight[tempch]++;

    int temp;
    cout << "输入数字“1”显示256种字节出现次数,输入其它数字则不显示!" << endl;
    cout << "请输入您的选择:";
    cin >> temp;
    cout << endl;
    if (temp == 1)
    {
        // 测试,显示256种字节出现的次数
        showWeight(weight);
        cout << endl;
    }

    cout << "文件读取完毕!\n"
         << endl;
    // 关闭文件
    fclose(in);

    // 将编码生成Huffman树
    // Huffman树共有n个叶子节点
    int n = 256;
    // 那么就有2n+1个节点
    int m = 2 * n - 1;
    // 定义Huffman树
    HuffmanTree pHT = new HTNode[m + 1];
    CreateHuffmanTree(pHT, weight, n);

    int temp1;
    cout << "输入数字“1”显示Huffman 树每个节点的信息,输入其它数字则不显示!" << endl;
    cout << "请输入您的选择:";
    cin >> temp1;
    cout << endl;
    if (temp1 == 1)
    {
        // 测试,输出 Huffman 树每个节点的信息
        TestHufTree(pHT);
        cout << endl;
    }

    // 生成Huffman编码
    char **pHC = new char *[n + 1]; // 编码
    for (int i = 1; i <= n; i++)
        pHT[i].weight = weight[i - 1];
    HuffmanCoding(pHC, pHT);

    int temp2;
    cout << "输入数字“1”显示字节的Huffman编码信息,输入其它数字则不显示!" << endl;
    cout << "请输入您的选择:";
    cin >> temp2;
    cout << endl;
    if (temp2 == 1)
    {
        // 测试,显示字节的Huffman编码信息
        cout << "\n哈夫曼树的编码信息为:" << endl;
        cout << "Byte\tHuffmanCode" << endl;
        TestHufCode(511, pHT, pHC);
        cout << endl;
    }

    // 计算编码缓冲区大小
    int nSize = 0;
    for (int i = 0; i < 256; i++)
        nSize += weight[i] * strlen(pHC[i + 1]);
    nSize = (nSize % 8) ? nSize / 8 + 1 : nSize / 8;

    // 对编码文件进行压缩
    char *pBuffer = NULL;
    pBuffer = new char[nSize];
    memset(pBuffer, 0, (nSize) * sizeof(char));
    Encode(pFilename, pHC, pBuffer, nSize);
    if (!pBuffer)
    {
        return ERROR;
    }

    HEAD sHead;
    InitHead(pFilename, sHead);
    cout << "原文件大小:" << sHead.length << "字节" << endl;
    int afterlen = WriteFile(pFilename, sHead, pBuffer, nSize);
    cout << "压缩后文件大小:" << afterlen << "字节" << endl;
    cout << "压缩比率:" << (double)afterlen * 100 / sHead.length << "%" << endl;

    delete pHT;
    delete[] pHC;
    delete pBuffer;

    return OK;
}

// 扫描文件和初始化头文件的信息
int InitHead(const char *pFilname, HEAD &sHead)
{
    // 文件类型
    strcpy(sHead.type, "HUF");
    // 源文件长度
    sHead.length = 0;
    for (int i = 0; i < SIZE; i++)
        // 权值
        sHead.weight[i] = 0;

    // 以二进制流形式打开文件
    FILE *in = fopen(pFilname, "rb");

    // 扫描文件,获得权重
    int ch;
    while ((ch = fgetc(in)) != EOF)
    {
        sHead.weight[ch]++;
        sHead.length++;
    }

    // 关闭文件
    fclose(in);
    in = NULL;
    return OK;
}

// 实现压缩编码
int Encode(const char *pFilname, const HuffmanCode pHC, char *pBuffer, const int nSize)
{
    // 打开文件
    FILE *in = fopen(pFilname, "rb");

    // 开辟缓冲区ni
    pBuffer = (char *)malloc(nSize * sizeof(char));
    if (!pBuffer)
        cout << "开辟缓冲区失败!" << endl;

    // 工作区
    char cd[SIZE] = {0};
    // 缓冲区指针
    int pos = 0;
    int ch;

    // 扫描文件
    while ((ch = fgetc(in)) != EOF)
    {
        strcat(cd, pHC[ch + 1]);
        // 压缩编码
        while (strlen(cd) >= 8)
        {
            pBuffer[pos++] = Str2byte(cd);
            for (int i = 0; i < SIZE - 8; i++)
            {
                cd[i] = cd[i + 8];
            }
        }
    }
    if (strlen(cd) > 0)
    {
        pBuffer[pos++] = Str2byte(cd);
    }

    fclose(in);

    return OK;
}

// 生成压缩文件
int WriteFile(const char *pFilename, const HEAD sHead, const char *pBuffer, const int nSize)
{
    // 生成文件名
    char filename[256] = {0};
    strcpy(filename, pFilename);
    strcat(filename, ".huf");

    // 以二进制流形式打开文件
    FILE *out = fopen(filename, "wb");

    // 写文件
    fwrite(&sHead, sizeof(HEAD), 1, out);

    // 写压缩后的编码
    fwrite(pBuffer, sizeof(char), nSize, out);
    // 关闭文件,释放文件指针
    fclose(out);
    out = NULL;

    cout << "生成压缩文件:" << filename << endl;
    int len = sizeof(HEAD) + strlen(pFilename) + 1 + nSize;
    return len;
}

// 将字符串转换成字节
char Str2byte(const char *pBinStr)
{
    char b = 0x00;
    for (int i = 0; i < 8; i++)
    {
        // 左移一位
        b = b << 1;
        if (pBinStr[i] == '1')
        {
            b = b | 0x01;
        }
    }
    return b;
}

5.5 Huffman.h

#pragma once

// Huffman 树节点
typedef struct
{
    int weight; // 权值
    int parent; // 父节点
    int lchild; // 左孩子
    int rchild; // 右孩子
} HTNode, *HuffmanTree;

// Huffman编码
typedef char **HuffmanCode;

// 显示 256 种字节的出现的次数
void showWeight(int weight[]);

// 生成Huffman树
int CreateHuffmanTree(HuffmanTree pHT, int weight[], int n);

// 生成Huffman编码
int HuffmanCoding(HuffmanCode &pHC, HuffmanTree &pHT);

// 查找 Huffman 树结点数组中权值最小的节点
void Select(HuffmanTree &HT, int i, int &s1, int &s2);

// 测试函数,输出哈夫曼树的每个节点信息
int TestHufTree(HuffmanTree pHT);

// 测试函数,采用先序遍历的方法,输出 Huffman 树每个节点的信息
void TestHufCode(int root, HuffmanTree &pHT, HuffmanCode &pHC);

5.6 Huffman.cpp

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <malloc.h>
#include <string.h>

#include "Huffman.h"
#include "global.h"

using namespace std;

// 显示 256 种字节的出现的次数
void showWeight(int weight[])
{
    cout << "原文件每个字符的权值为:" << endl;
    cout << "Byte\t"
         << "Weight\t" << endl;
    for (int i = 0; i < 256; i++)
        printf("0x%02X\t%d\n", i, weight[i]);
}

// 生成Huffman树
int CreateHuffmanTree(HuffmanTree pHT, int weight[], int n)
{
    int s1, s2, i;
    int m = 2 * n - 1;

    // 初始化
    for (i = 1; i <= n; i++)
    {
        pHT[i].weight = weight[i - 1];
        pHT[i].lchild = 0;
        pHT[i].rchild = 0;
        pHT[i].parent = 0;
    }
    for (i = n + 1; i <= m; i++)
    {
        pHT[i].parent = 0;
        pHT[i].lchild = 0;
        pHT[i].rchild = 0;
        pHT[i].weight = 0;
    }

    for (i = n + 1; i <= m; i++)
    {
        // 从pHT[1...i-1]中选择parent为0且weight最小的两个结点,其序号分别为s1和s2
        Select(pHT, i - 1, s1, s2);
        pHT[s1].parent = i;
        pHT[s2].parent = i; // 修改s1和s2结点的父指针parent

        pHT[i].lchild = s1;
        pHT[i].rchild = s2; // 修改i结点的左右孩子指针

        pHT[i].weight = pHT[s1].weight + pHT[s2].weight; // 修改权值
    }
    return OK;
}

// 查找Huffman树节点数组中权值最小的节点
void Select(HuffmanTree &pHT, int i, int &s1, int &s2)
{
    int minValue = 0x7FFFFFFF;

    // 找到最小的一个权值
    for (int j = 1; j <= i; j++)
    {
        if (pHT[j].parent == 0 && pHT[j].weight < minValue)
        {
            minValue = pHT[j].weight;
            s1 = j;
        }
    }

    minValue = 0x7FFFFFFF;
    // 找到倒数第二小的权值
    for (int j = 1; j <= i; j++)
    {
        if (j != s1 && pHT[j].parent == 0 && pHT[j].weight < minValue)
        {
            minValue = pHT[j].weight;
            s2 = j;
        }
    }
}

// 生成Huffman编码
int HuffmanCoding(HuffmanCode &pHC, HuffmanTree &pHT)
{
    // 无栈非递归遍历Huffman树,求Huffman编码
    // 记录访问路径
    char cd[256] = {'\0'};
    // 记录当前路径长度
    int cdlen = 0;

    // 遍历Huffman树时用做节点的状态标志
    for (int i = 1; i < 512; i++)
        pHT[i].weight = 0;

    int p = 511;
    while (p != 0)
    {
        // 向左
        if (pHT[p].weight == 0)
        {
            pHT[p].weight = 1;
            if (pHT[p].lchild != 0)
            {
                p = pHT[p].lchild;
                cd[cdlen++] = '0';
            }
            // 登记叶子节点的字符的编码
            else if (pHT[p].rchild == 0)
            {
                pHC[p] = (char *)malloc((cdlen + 1) * sizeof(char));
                cd[cdlen] = '\0';
                strcpy(pHC[p], cd); // 复制编码
            }
        }
        // 向右
        else if (pHT[p].weight == 1)
        {
            pHT[p].weight = 2;
            // 右孩子为叶子节点
            if (pHT[p].rchild != 0)
            {
                p = pHT[p].rchild;
                cd[cdlen++] = '1';
            }
        }
        // 退回父节点,编码长度减一
        else
        {
            pHT[p].weight = 0;
            p = pHT[p].parent;
            cdlen--;
        }
    }
    return OK;
}

// 测试函数,输出哈夫曼树的每个节点信息
int TestHufTree(HuffmanTree pHT)
{
    cout << "哈夫曼树的每个节点信息为:" << endl;
    cout << "Byte\t\tWeight\tParent\tLchild\tRchild\n";
    for (int i = 1; i < 512; i++)
    {
        // 判断语句为了对齐格式
        if (i <= 99)
            cout << "pHT[" << i << "]\t\t" << pHT[i].weight << "\t" << pHT[i].parent << "\t" << pHT[i].lchild << "\t" << pHT[i].rchild << endl;
        else
            cout << "pHT[" << i << "]\t" << pHT[i].weight << "\t" << pHT[i].parent << "\t" << pHT[i].lchild << "\t" << pHT[i].rchild << endl;
    }
    return OK;
}

// 测试函数,采用先序遍历的方法,输出 Huffman 树每个节点的信息
void TestHufCode(int root, HuffmanTree &pHT, HuffmanCode &pHC)
{
    if (root <= 1)
        return;

    if (pHT[root].lchild == 0 && pHT[root].rchild == 0)
        printf("0x%02X\t%s\n", root - 1, pHC[root - 1]);
    if (pHT[root].lchild) // 访问左孩子
        TestHufCode(pHT[root].lchild, pHT, pHC);
    if (pHT[root].rchild) // 访问右孩子
        TestHufCode(pHT[root].rchild, pHT, pHC);
}
  • 14
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

春天熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值