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

承接上文。这篇文章来聊聊状态机,并用状态机来解码 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 编码在文件压缩和解码中的应用,并通过实际的例子说明了如何使用这些技术。

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

相关推荐
Captain823Jack22 分钟前
nlp新词发现——浅析 TF·IDF
人工智能·python·深度学习·神经网络·算法·自然语言处理
Captain823Jack1 小时前
w04_nlp大模型训练·中文分词
人工智能·python·深度学习·神经网络·算法·自然语言处理·中文分词
m0_748247551 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
是小胡嘛2 小时前
数据结构之旅:红黑树如何驱动 Set 和 Map
数据结构·算法
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203982 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
呆呆的猫2 小时前
【LeetCode】227、基本计算器 II
算法·leetcode·职场和发展
Tisfy2 小时前
LeetCode 1705.吃苹果的最大数目:贪心(优先队列) - 清晰题解
算法·leetcode·优先队列·贪心·
NiNg_1_2342 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts