关于状态机的思考
LeetCode 上有这样一道题目:表示数值的字符串。题目看似简单,但其最佳解法却蕴含着一种绝妙的工程实践——状态机。
本文从这道编程题目出发,思考了一些状态机的使用场景,记录下脑暴过程。
题目描述如下:
什么是状态机
状态机的全称为有限状态自动机(Finite-State Machine,FSM)。状态机由『状态』和『动作』两部分组成,一个状态经过某个动作之后可以转移到下一个状态。
例如,一扇门可以通过状态机来描述。
门的状态包括:
- 开/open
- 闭/closed
可以施加的动作包括:
- 推/push
- 拉/pull
状态机的表示
由上面的描述可以知道,状态机本质上等效于一个有向图(可以有环):
- 状态 <–> 图的顶点
- 动作 <–> 图的边
众所周知,图有多种表示方式,除了画图外,还有邻接表、邻接矩阵等。
上例中的状态机以邻接表的形式表示如下:
1 | { |
以邻接矩阵的形式表示如下:
PS:这只是个特例,状态图一般是非常稀疏的。
状态机怎么用
以上述 LeetCode 问题的解答为例逐步引出状态机的妙用。
PS:以下解答摘自官方题解和评论区。
解法 1. 暴力穷举
思路是通过 if...else
逻辑穷举所有可能的情况:
1 | class Solution { |
不足:
- 冗长,可读性差
- 难以确认代码逻辑的正确性
解法 2. 状态编程
比起暴力穷举,通过声明一些全局变量来保存状态,可复用一部分代码逻辑。
1 | class Solution { |
不足:
- 状态变量多,不容易读懂
- 难以确认代码逻辑的正确性
解法 3. 状态机
在解法 2 的基础上进一步抽象,整理出各种状态以及可能遇到的字符的类型。
可以发现状态和类型都是有限的,并且它们之间的转移关系是确定的。如下图:
邻接矩阵:
邻接表:
1 | { |
代码实现:
1 | from enum import Enum |
优势:
- 状态表的含义一目了然,含义清晰
- 可以遍历状态转移关系确认代码的正确性
- 代码量少,逻辑简单
为什么要使用状态机
状态机不是一个良好的算法,而是一个优秀的工程实践! 因为状态机本质上还是穷举,既没有降低时间复杂度,也没有降低空间复杂度。
使用状态机的好处:
- 强制理清思路,不容易出错。
- 方便修改和扩展。尤其是在工程上线后,不需要修改代码,调整邻接表配置即可。
- 高度抽象。将繁琐的
if...else
逻辑抽象成一套公共库,节约开发成本。在 Java、Python 等热门语言中都有大量的状态机库可供调用。
状态机的应用
有『状态』和『变化』的地方就可以有状态机。
- 编译器/解释器
- 如何识别语法错误?例如括号不闭合、
int a = /2
。
- 如何识别语法错误?例如括号不闭合、
- 正则表达式
\d+
与1
,123a
match 不上,中间发生了什么?
- 网络协议,如 TCP
- 握手阶段在等待 ACK 的过程中不接受 SYN、不接受数据。
- 订单系统
- 付款、取消、等待、投诉等动作会对订单状态产生什么影响?
- 游戏任务设计
- 和 NPC 对话后再做某项任务与直接做某项任务效果不同。
- 一种全面、系统的思考问题的方式,可用于做问题建模。例如:
- Airflow 中的任务状态建模:
- 任务 pending 状态发起重试会发生什么?
- 任务运行中状态置成功会发生什么?
- 对 pending 任务发起回溯会发生什么?
- 对 pending 任务修改调度周期会发生什么?
- 一些权限系统中的糟糕设计:
- 自己创建的资源,默认没权限,需要自己给自己申请权限
- 管理员可以移除任何人的权限,但把自己的权限移除了
- Airflow 中的任务状态建模: