初探有限状态自动机---用状态机讲解防抖与节流

1,067 阅读13分钟

引言

有限状态自动机(finite-state machine,FSM或finite state automata, FSA),也被称为有限状态机或状态转移系统,是一个抽象的计算模型。它由一组状态、一组输入字符以及一组状态转移函数组成。其中,状态代表一个特定的情况,输入字符代表状态转移的触发条件,状态转移函数描述了在给定输入条件下从一个状态到另一个状态的转移过程。

在计算机科学中,有限状态自动机被广泛应用于编译器、词法分析器、文本处理和自然语言处理等领域。对于编译器和词法分析器,有限状态自动机可以将源代码解析成标记流或抽象语法树;对于文本处理和自然语言处理,有限状态自动机可以实现关键字匹配、嵌套分析和词法分析等操作。此外,有限状态自动机还可以用于模拟各种实际系统,如交通信号系统和电子电路等。

有限状态自动机分为确定性有限状态自动机(DFA)和非确定性有限状态自动机(NFA)两种类型。本文着重介绍确定性有限状态自动机。DFA 中的每个状态只能根据当前输入字符转移到唯一的下一个状态,而 NFA 中的每个状态可能对应多个下一个状态。因此,NFA 的状态转移函数需要额外考虑 epsilon 转移,即在不消耗任何输入字符的情况下,自动机可以从一个状态转移到另一个状态。

在实践中,我们通常使用状态图来描述一个有限状态自动机,其中节点表示状态,边表示状态之间的转移,并用输入字符标记每条边。使用状态图可以更加直观地理解状态机的行为。

有限状态自动机是一个重要的计算模型,可以用于模拟各种实际系统,也是许多计算机科学问题的基础。有限状态自动机,同时也是《编译原理》,《离散数学》等计算机科学中的中药概念。同时,学习有限状态自动机还需要掌握正则表达式和文法等相关概念,这些知识对于理解和应用有限状态自动机都非常重要。

FSM 的数学定义

定义

有限状态自动机指五元组:

M=(S,I,f,A,S0)M=(S,I,f,A,S_0)

  1. SS 是一个有限的状态集合
  2. II 是一个有限的输入符号集合
  3. ff 表示状态的转换是从 SIS*ISS 的函数
  4. 可接受状态的非空集合 ASA \subseteq S
  5. 初始状态 S0S_0

我们用通俗的语言来解释下上面的第三条。

它表示读入了一个状态(或者是当前的状态)是 SS 及当前的输入符号 II ,由这两者来共同决定下一个状态。

下面我们用例子来理解上述数学定义。

M=({S0,S1},{a,b},f,{S1},S0)M=(\{S_0,S_1 \},\{a,b\},f,\{S_1\},S_0) 构成一个有限状态自动机,其中

f(S0,a)=S0,f(S0,b)=S1f(S_0,a)=S_0,f(S_0,b)=S_1

f(S1,a)=S1,f(S1,b)=S0f(S_1,a)=S_1,f(S_1,b)=S_0

问题:那么什么形式的字符串才会让我们最终停留在状态S1S_1呢?

根据上面的定义我们知道,可接受的状态为S1S_1S0S_0为初始状态。f(S0,a)=S0f(S_0,a)=S_0代表的就是如果当前的状态是S0S_0,读入了aa,那么就会转换为S0S_0状态。

经过简单的尝试,我们不难发现,当字符串为包含奇数个bbaba-b串就是我们想要的。例如:abbbaabbba,abbaababbaab ,当然这可能需要我们一些时间去尝试,这比较抽象,由此,我们引申出FSM的两种表现方式。

表现方式

表格

一个有限状态自动机的转移函数ff还有表格的表现方式:

图片1.png

状态转移图

有限状态自动机的状态转移图是一个有向图。

  1. 顶点表示状态集合 SS 中各个元素
  2. 通过在有向边上表明输入符号表示 ff
  3. 接受状态用双圈表示(备注:本文章中,没有强制这个状态的样式,一般在一些严格的考试中是需要注意的,但在工作中的大多数情况,我们默认了本文中的使用方式,你可以将最终可接受的状态颜色进行标识区分)
  4. 使用一个箭头指向表示开始状态的顶点

如下图所示:

1.png

根据这些规则,我们就可以把上面表格和题目中的有限状态自动机的状态转移图画出来了。

2.png

通过状态转移图,我们就可以很直观的看出来,当输入的字符串中b的数量为奇数时,我们的状态机最终就会停留在 S1S_1 状态,也就是我们的可接受状态。

例子

下面我们来看两个例子(默认输入字符只有a,b):

  • 画出接受所有偶数长度的串的有限状态自动机

分析:一个串的长度有两种可能,偶数长度,奇数长度,奇数长度不可接受,偶数长度才可接受,那么读入奇数个字符后,再次读入就肯定是偶数个字符,反之亦然。所以状态机中有2个状态,一个代表已经读入了偶数个字符,一个代表已经读入了奇数个字符。最初的时候我们认为已经读入了0个字符,所以 S0S_0 就是可接受状态,于是我们得到:

11.png

  • 画出接受所有以b结尾的串的有限状态自动机

分析:一共就有两种可能性,一个是以a结尾,一个是以b结尾,读到的最后一个字符是b就是可接受状态,读到的最后一个字符是a,那么我在的状态就是不可接受状态。 无论我在哪个状态,只要我读到了b,就会到可接受状态,而读到了a,就会离开可接受状态到不可接收状态,于是我们得到:

9.png

值得注意的是,在本例子中,我们是将 S1S_1 作为可接受状态。

FSM 的实际应用

闸机

v2-a49c17659d76b6d99bd4e0844af1abd9_b.jpg

一个生活中很常见的例子,地铁中或者游乐园入口处的闸机。初始的时候,闸机是锁住的状态,阻止人通过。只有刷了卡,闸机才会处于解锁状态,才允许人用力推开,从而进入。在人通过后,闸机再次处于锁住的状态,直到下次有人刷卡。

通过这个真实的案例,我们来进行分析,我们可以认为闸机有两种状态,”锁住“和”解锁“,有两种可能的输入可以影响它的状态。一是刷了卡,二是推动闸机的栏杆。在锁定状态下,无论你怎么推,闸机始终处于锁定状态。当你刷卡后,闸机就会从锁住状态变为解锁状态。在解锁状态时,你再次进行刷卡将不会有任何效果,也就是说,你再次刷卡后,闸机的状态并没有发生改变。当人用手去”推“了闸机的栏杆后,给出了一个”推“的输入,闸机的状态又变为了“锁住”的状态。

我们用上面讲过的状态转移函数表格的表现形式来说明:

工作簿1.png 根据表格,我们很容易画出它的状态转移图:

Untitled.png

开关

在这个例子里面我们来看一个开关的示例.开关是一个很简单的设备,它有”开”和”关”两种状态.在这个示例中我们将会对这个行为做一些小小的改变.这是一个人为的修改需求,在实际生活中,没有一个厂家会这么做,但是却能很好的阐释我们对于有限状态自动机的理解.

我们先来看最简单的状态图.

Untitled.png

我们针对这两种状态,专门来添加一些”子状态”来修改这个系统的行为.在开始之前,我们先来简单的描述下上面这个状态图的行为:

  • 无论何时”轻击”这个event发生,状态机都将在”开”和”关”这两个状态之间进行切换.
  • 当状态机”进入/离开”某一个状态时,它将会触发一个与之相对应的”打开”或者”关闭”的动作.

接下来我们将会在”关”这个状态下定义一些”子状态”,你可以理解为,我们又添加了一些新的需求.

改善”关”状态

在”关”的状态下,如果我们”轻击”了开关,需要过2s后才可以变为”开”的状态.也就是说,如果在这2s的等待时间中,你又再次”轻击”了开关,那就需要再等2s后才可以变为”开”的状态.我们用状态图来说明:

Untitled.png

现在”关”这个状态有了自己的一个小小的状态机.

需要解释说明的是, AA 状态是一个初始状态,所以无论何时,一旦进入到”关”这个状态后,我们将自动进入到 AA 状态.当一个状态机像这样同时处于两种状态的时候,处于最深层的那一个会最先去处理到达的事件,所以当状态机在 AA 状态的时候,”轻击”事件后,状态仍旧会回到 AA 状态.

AABB 的”延迟”转换会导致状态机进入到 BB 的状态,但是当且仅当状态机曾经在状态 AA ,在2s内没有被打断.

当我们处在 BB 状态的时候,它并不关心”轻击”事件是否发生了.所以,这个内部的小状态机的父亲状态会去处理这个事件,从而导致了开关的状态处于”开”.

这是一个简单的”防抖”行为,值得注意的是,在目前的状态机中我们只对”关”这个状态做了”防抖”

改善”开”状态

我们想要延迟0.5s才打开灯,但是如果在这个期间,”轻击”了开关,都允许我们转变为”关”的状态.状态图如下所示:

Untitled.png

我们对”开”这个状态,增加了”子状态”,与”关”的”子状态”有些不同的是, CCDD 这两个状态都没有去处理”轻击”这个event,按照我们的约定,如果当前处在”开”这个状态,如果”轻击”事件发生了,状态机将会立即回退到”关”的状态.

有趣的但是值得我们注意的是,”关闭开关”这个动作被调用的前提是,”打开开关”这个操作被调用过.因为只有曾经达到过 DD 状态,才有可能有离开 DD 状态.

进一步细化”开”的状态

在我们这样设计好灯的开关后,我们测试了一种使用场景:开灯,然后在0.6s后,关灯.按照这个操作流程,我们的灯在亮了0.1s后,就灭了,市场部门说,这会让客户觉得很奇怪,所以我们需要加一条需求,当等被打开后,至少要亮0.5s后才可以灭.在状态图上,我们很轻松就可以做到这点;我们处理的方法是在 DD 状态中再添加几个”子状态”,这些子状态将会处理来自前0.5s内的”轻击”事件,而且在这0.5s内不会做任何事情.如下所示:

Untitled.png

我们在 DD 状态中新添加了 EE 状态和 FF 状态:

  • EE 状态用来忽略”轻击”动作事件
  • FF 状态用来处理事件

“开”状态与”关”状态下处理”轻击”的不同

在”关”的状态下,当连续”轻击”开关的时候,灯一直是处于”关”的状态的.这一系列的”轻击”事件都将被忽略.只有在”轻击”事件2s都没有发生了,下次的轻击事件才会让灯亮起来,也就是状态机的状态处于了”开”.也就是说”轻击”事件在能够产生影响之前必须有2s的冷静期.

在”开”状态下,连续的”轻击”,在前0.5s的时候都是被忽略的,只有在0.5s以后,”轻击”事件才会让灯关掉.

看完这个例子,你是不是对”防抖”与”节流”又多了一层理解呢?

FSM 工具推荐

如上的示例,为了方便理解,所举均简单易懂,但是在日常工作中,我们所碰到的状态机往往比较复杂,且可能需求不断变化,手动维护状态转换表或状态转移图会面临很大得挑战,因为他们需要将大量的时间与精力倾注在如何管理好状态机中的各种状态上,而不是程序本身的运行逻辑。所以,我们可以利用一些工具来帮我们自动生成状态转移图.

FSME

640.jpg

FSME是一个基于Qt的有限状态机工具,它能够让用户通过图形化的方式来对程序中所需要的状态机进行建模,并且还能够自动生成用C++或者Python实现的状态机框架代码。

该软件可在Windows以及Linux上安装使用.详细的使用方法可以参考本链接:fsme.sourceforge.net/

XState

用于现代 Web 的 JavaScript 和 TypeScript 的有限状态机和状态图.

XState是一个库,用于创建,解释和执行有限状态机和状态图,以及管理这些状态机与演员(Actor)调用.

演员模型

演员模型是另一个非常古老的数学模型,它与状态机配合得很好。 它指出,一切都是一个“演员”,可以做三件事:

  • 接收 消息

  • 发送 消息到其他 Actor

  • 用它收到的消息做一些事情( 行为),比如:

    • 改变它的本地状态
    • 发送消息到其他演员
    • 产生 新的演员

演员的行为可以通过状态机(或状态图)来描述。

小结

本文讲述了有限状态自动机的简单概念及应用,可以视作为入门有限状态自动机的基础文章,能够理解状态机的原理,对于我们日常写程序,设计电子电路等有非凡的意义,它可以让你远离 ifelseif-else 的嵌套地狱,让你在编码阶段就能根据状态机识别出程序潜在的风险;同时,值得我们广大开发,测试,产品人员学习实践,相信我,当你习惯了使用有限状态自动机来解决编程中的问题,你会觉得打开了另一扇大门.由于本篇文章篇幅有限,很多内容没有做展开讲述,例如:

  • 非确定性有限状态自动机的数学定义,理解,定义,应用;
  • 有限状态自动机的扩展;
  • 如何在编程中具体使用有限状态自动机解决实际问题,例如使用XState进行编程.

这些内容,作者将会在后续文章中逐一涵盖,希望大家能够在了解,熟悉后状态机后,能够多多在工作实践中进行应用,欢迎各位小伙伴共同交流心得.