🥳前端算法面试之状态机-每日一练

承接上文。这篇文章来聊聊状态机,并用状态机来解码 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 编码,主要分一下几个部分:

  1. 状态机:简要介绍了状态机的基本概念和原理。
  2. 解码:简单描述了 Huffman 解码的几种方法
  3. 代码实现:通过状态机的方式实现解码 Huffman 编码,将其转换回原始字符。并且动态生成了状态机代码

总的来说,这篇文章深入探讨了状态机和 Huffman 编码在文件压缩和解码中的应用,并通过实际的例子说明了如何使用这些技术。

什么问题可以评论区留言哦。我每天都会分享一篇算法小练习,喜欢就点赞+关注吧

相关推荐
uhakadotcom8 分钟前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰15 分钟前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪23 分钟前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪31 分钟前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy1 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom2 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom2 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom2 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom2 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试