栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

[C++] 使用 Huffman Tree 哈夫曼树实现对 ASCII 字符串文本的压缩与解压(下)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

[C++] 使用 Huffman Tree 哈夫曼树实现对 ASCII 字符串文本的压缩与解压(下)

上接:
[枫铃树] [C++] 使用 Huffman Tree 哈夫曼树实现对 ASCII 字符串文本的压缩与解压(上)

C++ 方式实现

在上篇中,我们已经成功构建一棵哈夫曼树。
本篇中,我们将使用该哈夫曼树完成字符串的编码,及对编码后的字符串的解码。

编码

借助之前生成的哈夫曼树,对传入文本进行编码,编码结果写入到目标字符串中。所以,函数声明如下:

bool encode(Node* node, const char* src, char* dst, bool isEntry = true);

注:这里采用深度优先搜索方式寻找字符。这个方法显然不好,因为它的效率太低了。但是,考虑到笔者水平较差,暂仅用这种方式完成。更好的完成方式留给聪明的你思考。


参数表说明
  • node: 当前所在节点(外部调用时,这个点为哈夫曼树的根。后续递归时,这个点为某子树的根,或叶节点)。
  • src: 原始字符串。
  • dst: 存储编码结果的字符串。
  • isEntry: 这个变量的设计比较复杂。解释之前,需要先思考一下 encode 函数是如何工作的。

encode 函数工作过程

对于单个字符,搜索过程中,每经过一个非叶节点,就要先将0写入到dst,然后尝试向左搜索。如果向左没有找到对应字符,则将刚写入的0改为1,然后向右寻找。如果依旧寻找失败,则擦除写入的内容,然后向调用者报告“未找到”(报告即返回特定信号)。首先,应该有两个标记,分别标记 src 中的读取位置(记为 readerOffset,初始为 0)和 dst 中的写入位置(记为 writerOffset,初始为 0)。
上述过程仅完成对单个字符的编码。事实上,我们要对好多字符进行编码,直到读到尾零()。对此,encode 需要同时控制 readerOffset 的移动,然后对目前指向的字符尝试写入。
不难看出,encode 具有两个身份:控制读入,深搜写入。我们需要区分当前执行时,encode 需要做什么。于是可以引入 isEntry 变量。如果它是 true,表明该 encode 的任务是控制读取。反之,对当前指向的字符进行写入。
所以,当我们在外部调用 encode 时,应该始终让 isEntry 为 true (使用其默认值)。内部递归调用时,因始终令 encode 为 false.

返回值

对于控制读入的 encode 函数调用,不需要向上级报告什么信息。对于控制写入的 encode 函数,需要向上级报告自己有没有找到对应字符。
对于叶节点,如果当前节点代表的字符正好是要编码的字符,就返回 true. 否则返回 false.
对于非叶节点,如果它的某个叶节点成功报告了 true, 则它也报告 true,表明它所在的子树成功完成单字符的编码。否则,报告 false.

函数实现

进入时,声明读入位移和写入位移这两个静态局部变量。

static int writerOffset = 0;
static int readerOffset = 0;

如果函数的功能是控制读取,那么很简单,先对静态局部变量做清零(防止多次编码时,变量值还是前一次的值),然后对每个字符做深搜查找即可。

if (isEntry) { // 控制读取
    writerOffset = 0;
    readerOffset = 0;

    while (src[readerOffset] != '') {
        huffmanEncodeDFS(node, src, dst, false);
        readerOffset += 1;
    }
    
    target[writerOffset] = ''; // 在目标字符串结尾补充尾零。
    return true; // 无所谓。
} else {
    ... // 控制写入
}

很简单是不是!
下面看写入过程。我们这样设计:
当我们处于某个非叶节点,我们先直接将向左这个信息写入到结果中。如果向左没找到,就将向左改为向右。如果向右也没找到,则擦除写入的内容,并报告 false.
不难发现,哈夫曼树的节点,要么同时拥有左右孩子,要么同时没有左右孩子。
因此,如果一个节点没有左孩子,则表明它是叶节点。从上述设计可以看出,它的任务很简单,报告自己代表的字符是否和当前正在读的字符一致就行了。

if (node->lc == nullptr) {
    // 位于叶节点
    return src[readerOffset] == node->c;
} else {
    ... // 位于非叶节点
}

对于非叶节点,向左向右尝试即可。

// 尝试向左
dst[writerOffset] = '0';
writerOffset += 1;
if(huffmanEncodeDFS(node->lc, src, dst, false)) {
    // 左节点命中
    return true;
} else {
    // 左节点未命中。
    dst[writerOffset-1] = '1';
    // 尝试向右
    if (huffmanEncodeDFS(node->rc, src, dst, false)) {
        // 右节点命中。
        return true;
    } else {
        // 右节点未命中。
        writerOffset -= 1; // 相当于擦除。
        return false;
    }
}

至此,字符串编码完毕。

解码

相对编码,解码过程显得十分简单。大体思路与编码类似,但是递归方向不再是到处撞,而是跟着已编码字符串的指引前进。
函数定义如下:

void decode(Node* node, const char* encodedStr, char* decodedStr, bool isEntry = true);

其中,decodedStr 用于存放解码后的字符串。
isEntry 设计与编码类似。


对于控制读入的部分,很简单:

static int writerOffset = 0;
static int readerOffset = 0;

if (isEntry) { // 控制读入部分。
    writerOffset = 0;
    readerOffset = 0;

    while (encodedStr[readerOffset] != '') {
        decode(node, encodedStr, decodedStr, false);
    }

    decodedStr[writerOffset] = '';
} else {
    ... // 控制写入部分。
}

写入时,只要没有达到叶节点,就跟着已编码字符串的指引继续向下走。
到达叶节点时,将叶节点对应字符写入结果即可。

if (node->lc == nullptr) {
    // 抵达叶节点。
    decodedStr[writerOffset] = huffmanTree->c;
    writerOffset += 1;
} else {
    readerOffset += 1;
    if (encodedStr[readerOffset-1] == '0') {
        decode(node->lc, encodedStr, decodedStr, false);
    } else {
        decode(node->rc, encodedStr, decodedStr, false);
    }
}

至此,解码函数编写完成。基于以上核心代码,额外制作如横向打印二叉树、输入输出控制的工具函数,即可轻松完成上篇中的演示程序。


当然,程序结束前,不要忘了清理动态申请到的内存。
从哈夫曼树的根节点进入,递归释放即可。

void cleanup(Node* node)
{
	if (node->lc != nullptr) {
		cleanup(node->lc);
	}
	if (node->rc != nullptr) {
		cleanup(node->rc);
	}
	delete node;
}

以上便是使用哈夫曼树完成对 ASCII 字符串文本的压缩与解码方法。感谢你的耐心阅读,希望对你的学习和工作有所帮助。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/443775.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号