[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)。
上述过程仅完成对单个字符的编码。事实上,我们要对好多字符进行编码,直到读到尾零(\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] != '\0') {
        encode(node, src, dst, false);
        readerOffset += 1;
    }
    
    target[writerOffset] = '\0'; // 在目标字符串结尾补充尾零。
    return true; // 无所谓。
} else {
    ... // 控制写入
}

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

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

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

// 尝试向左
dst[writerOffset] = '0';
writerOffset += 1;
if(encode(node->lc, src, dst, false)) {
    // 左节点命中
    return true;
} else {
    // 左节点未命中。
    dst[writerOffset-1] = '1';
    // 尝试向右
    if (encode(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] != '\0') {
        decode(node, encodedStr, decodedStr, false);
    }

    decodedStr[writerOffset] = '\0';
} 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 字符串文本的压缩与解码方法。感谢你的耐心阅读,希望对你的学习和工作有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值