承接上文。这篇文章来聊聊状态机,并用状态机来解码 huffman 编码
状态机很神秘吗,一点也不
huffman 编码是可以用在压缩文件上面的,但压缩了之后需要解压成源文件的。那么解压的过程涉及到 huffman 编码的识别,也就是将 huffman 编码和字符对应的过程。
举个例子,下面是字符和 huffman 编码对应关系:
javascript
{ f: '000', g: '001', c: '01', d: '10', a: '110', b: '111' }
f 对应 000,g 对应 001 等。
然后,按照上面的对应关系,将下面的 huffman 编码解码成字符
javascript
110110111111010101101010000001
匹配的过程可以放心地采用贪心策略,即匹配到一个字符,就继续匹配下一个。在 huffman 中,一个字符的编码不会是另一个字符编码的前缀,因为每个字符都是 Huffman树的叶子节点。
不会出现这种情况: a:00 b:001 a 的编码是 b 编码的前缀
可以试试手动解码,解码之后的结果是:"aabbcccdddfg"
那么用代码怎么实现解码的过程呢?
方法一
可以先读取一个编码(0 或者 1),然后看看有没有对应的字符,没有的话,在读入一个编码,和之前的编码合并,看看是否有对应的字符。就拿上面的 huffman 编码举例子。
先读入 1,没有对应的字符;再读入一个 1,现在是 11, 没有对应的字符;再读入 0,现在是 110,有 a 对应。解码了一个字符。接着继续遍历编码串,直到遍历完所有的编码串,解码就完成了。
这样做效率是不是有点低啊
方法二
根据字符和编码的对应的关系,再此构建一颗 huffman 树,解读编码串的过程就是遍历 huffman 树的过程。读入了 1,就往右节点走一步;读入了 0,就往左节点走一步,直到叶子节点,就成功解读了一个字符。然后重新从 huffman 根部继续遍历,解读下一个字符。
这样可以 ok 呀,效率还是蛮高的
方法三
方法三采用的是状态机方法
状态机
状态机是一种用于描述状态转移的数学模型,它可以用于解决各种序列解码问题,包括Huffman树的解码。
状态机包含状态和转移。状态表示当前系统或者程序的运行状态,而转移表示状态之间的变化或者变化原因。在状态机中,每个状态可以有多个转移,每个转移可以依赖于当前状态或者当前输入来决定。
在Huffman树的解码过程中,需要根据编码字符串和Huffman树来还原原始数据。解码过程可以看作是从根节点开始,沿着路径向下查找,直到到达一个叶子节点。在这个过程中,需要根据当前节点的值来判断下一个节点的值。如果当前节点是0,那么下一个节点是左子节点的值;如果当前节点是1,那么下一个节点是右子节点的值。
状态机不仅仅可以用在 huffman 的解码,还有一个经典应用--代码编译。在代码编译过程中,会一个字符一个字符的读入状态机,状态机会根据传入的字符判断是否更改当前的状态,或者更改为什么状态。
比如在两个数字之间读入了一个 < , 这是一个小于号,将当前状态设置为比较大小中的"小于号状态"
。若之后又读入了一个"="
,那么状态就变成"小于等于号状态"
了
状态机虽然有些难懂,但是代码很简单,好懂
代码实现
javascript
const relationShip = { f: "000", g: "001", c: "01", d: "10", a: "110", b: "111" };
const code = "110110111111010101101010000001";
const decode = (code) => {
let res = "";
for (let i = 0; i < code.length; ) {
let next = start(code[i]);
while (typeof next == "function") {
i++;
next = next(code[i]);
}
res += next;
}
console.log(res);
};
//状态机s
function start(char) {
if (char == "0") return l;
return r;
}
function l(char) {
if (char == "0") return ll;
return lr;
}
function ll(char) {
if (char == "0") return lll;
return llr;
}
function lll() {
return "f";
}
function llr() {
return "g";
}
function lr() {
return "c";
}
function r(char) {
if (char == "0") return rl;
return rr;
}
function rl(char) {
return "d";
}
function rr(char) {
if (char == "0") return rrl;
return rrr;
}
function rrl() {
return "a";
}
function rrr() {
return "b";
}
上面是用状态机的实现 huffman 编码的全部代码了。是不是很好懂
调用状态的核心逻辑:如果状态机函数返回的是函数,说明解码还没有结束,所以继续调用 next
函数;如果返回的不是函数,就说一个字符的解码完成,将结果保存到 res
变量中,然后继续下一个字符的解码。
状态机的代码也很清晰,就是一个函数嵌套着另一个函数。
看看实际执行结果:
javascript
decode(code);
//aabbcccdddfg
结果正确
状态机代码的缺点
不过状态机有状态机的缺点,那就是长,代码很长,长就容易出错。
还有一个缺点:代码是死的。
想利用状态机解码,一但字符和编码的对应关系改变,状态机的代码就需要变。不像代码编译的状态机,语法不怎么变,状态机代码也就不用变。所以在灵活性这一块,还不如前面的第二种方法。
那怎么办呢,放弃这种方法吗?好可惜哦,再想想?....
动态生成代码
仔细观察状态机的代码,其实是有规律的,我们可以动态地根据字符和编码对应的关系,来动态生成状态机的代码
函数名称中的 l,表示左边;r 表示右边。
这里借用了遍历 huffman 树的思想,如果读入了 0,就往左走。状态机代码中是返回一个新的函数,新函数名称比调用者多了个 l。 如果读入了 1,就往右走。状态机代码中处理的是返回一个新的函数,新函数名称比调用者多了个 r。
举个例子,如果函数的名称是 ll,说明已经读入了两个 0,然后判断传进来的 char 是 0 还是 1。如果是 0,就返回 lll;如果是 1,就返回 llr。如果 00 有对应的字符,就直接返回对应的字符,不返回函数。
下面看代码实现:
javascript
const generateFunc = (code) => {
const res = {};
Object.entries(code).forEach(([label, relateCode]) => {
const funcName = relateCode.replace(/1/g, "r").replace(/0/g, "l");
res[funcName] = `function ${funcName}(char){return '${label}'}`;
});
res["start"] = "function start(char){if(char == '0'){return l;}return r;}";
const _generateFunc = (funcName) => {
const lname = funcName + "l";
if (!res[lname]) {
res[lname] = `function ${lname}(char){if(char=='0') return ${lname + "l"}; return ${lname + "r"}}`;
_generateFunc(lname);
}
const rname = funcName + "r";
if (!res[rname]) {
res[rname] = `function ${rname}(char){if(char=='0') return ${rname + "l"}; return ${rname + "r"}}`;
_generateFunc(rname);
}
};
_generateFunc("");
//check
console.log(Object.values(res));
return Object.values(res).join(";");
};
代码分析:
代码中定义了一个 generateFunc 函数,该函数接受一个字符串 code 作为参数。
首先初始化一个空对象 res,用于存储生成的函数。然后使用 Object.entries 遍历输入的 relationShip 对象,这里的作用是先生成直接返回字符的函数。
其中还定义一个递归函数 _generateFunc,用于递归生成剩余的函数。在 _generateFunc 函数中,首先检查是否已经生成过函数 lname 和 rname。如果已经生成过,则直接返回;否则,根据函数名生成函数体,并将其放入 res 对象中。然后递归调用 _generateFunc 函数,生成剩余的函数。
代码还是很简单的
测试代码
javascript
const funcString = generateFunc(relationShip);
eval(funcString);
decode(code);
这是打印结果:
完全正确, 缺点解决了耶✌️
总结
本文主要分享如何用状态机解码 huffman 编码,主要分一下几个部分:
- 状态机:简要介绍了状态机的基本概念和原理。
- 解码:简单描述了 Huffman 解码的几种方法
- 代码实现:通过状态机的方式实现解码 Huffman 编码,将其转换回原始字符。并且动态生成了状态机代码
总的来说,这篇文章深入探讨了状态机和 Huffman 编码在文件压缩和解码中的应用,并通过实际的例子说明了如何使用这些技术。
什么问题可以评论区留言哦。我每天都会分享一篇算法小练习,喜欢就点赞+关注吧