有限状态机的例子


25

我正在寻找有限状态机的好例子。语言并不是特别重要,只是很好的例子。

代码实现很有用(通用伪代码),但是收集FSM的各种用法也非常有用。

示例不一定需要基于计算机,例如Mike Dunlavey的Railroad网络示例非常有用。


12
正则表达式是有限状态机。
chrisaycock 2011年

5
我不明白为什么将此问题标记为“不具有建设性”。考虑到自我首次提出封闭式答案以来已经过去了将近2年,我认为这实际上是非常具有建设性和主题性的。
Aqua

2
@aqua-实际上更多的是堆栈问题。程序员,其任务是回答具有一个答案的问题,非常非常具体的问题。但是,基于Stack站点的定义,诸如此类的有价值的问题不会被认为是“建设性的”(在IMNSHO中,该术语的定义很差)。坦率地说,为使程序员真正有用,它应该对这个特定规则不那么热心地坚持,但是我年纪大,累了,或者出于其他目的,将必要的精力投入到“试图解决愚蠢问题”中。
ocodo 2012年

1
实际上,真正的问题是,Stack网站是众所周知的,协作的,格式良好且可读的格式的极少数高质量资源之一。看来,这种还原的堆栈,真正指向需要的网站格式,是教育“问题”(可能不使用E字。)
ocodo

3
我仍然恳求人们重新提出这个问题,因为更多的例子将是很好的。可悲的事实是,如果不添加新的答案,问题将一直存在。
ocodo 2012年

Answers:


28

安全(事件已触发)

  • 美国:多“锁定”状态,一个“解锁”状态
  • 过渡:正确的组合键/键会将您从初始锁定状态移动到更接近解锁状态的锁定状态,直到最终解锁。不正确的组合键/键会使您回到初始锁定状态(有时称为idle)

交通灯(触发时间|触发传感器[事件])

  • 状态:红色,黄色,绿色(最简单的示例)
  • 转换:在计时器后,将红色更改为绿色,绿色更改为黄色,并将黄色更改为红色。也可以在各种(更复杂)状态下感应汽车时触发。

自动售货机(事件触发,保险箱的变体)

  • 状态:IDLE,5_CENTS,10_CENTS,15_CENTS,20_CENTS,25_CENTS等,VEND,CHANGE
  • 过渡:在插入硬币,纸币后状态会发生变化,在正确的购买金额(或更多)后过渡到VEND,然后过渡到CHANGE或IDLE(取决于自动售货机的道德程度)

+1,如果可以的话。除了最后一个,其他一切看起来都不错。应该是IDLE,VEND和CHANGE。这些值是有条件的,应表示为IDLE与自身之间的过渡。您还需要一个表示已选择项目的状态。
伊万·普赖斯

@EvanPlaice:不会只是选择一个项目来触发从IDLE到VEND的更改吗?除非您设想要在售卖前确认选择的方法。
米斯科

这两个图表有机会吗?
ocodo 2012年

这些与FSM Wikipedia页面中提供的示例相同,该页面还包括电梯:“简单的示例是自动售货机,当存入正确的硬币组合时便会分配产品,电梯的停止顺序由所请求的楼层决定包括乘员,交通灯(在等待汽车时改变顺序)和密码锁(需要按正确的顺序输入密码)。”
icc97

1
@ icc97 FSM例子在日常生活中很多且普遍。顺便说一句,堆栈交换预发布日期的维基百科页面:)的例子中包含的信息
AQUA

14

边界网关协议示例

BGP是支持Internet上核心路由决策的协议。它维护着一个表,用于确定来自给定节点的主机的可达性,并使互联网真正分散。

在网络中,每个BGP节点是对等体,并且使用有限状态机,其中一个六个状态空闲连接活动OpenSentOpenConfirm,和建立。网络中的每个对等连接都维护以下状态之一。

BGP协议确定发送到对等方以更改其状态的消息。

BPG状态图。

BGP状态图

第一状态为空闲。在这种状态下,BGP初始化资源,拒绝入站连接尝试,并启动与对等方的连接。

连接

第二种状态Connect。在此状态下,路由器等待连接完成,如果成功,则转换到OpenSent状态。如果不成功,它将重置ConnectRetry计时器,并在到期时转换为活动状态。

活性

活动状态下,路由器将ConnectRetry计时器重置为零,并返回到Connect状态。

开放发送

OpenSent状态下,路由器发送一个Open消息并等待返回一条消息。交换Keepalive消息,并在成功接收后将路由器置于“已建立”状态。

成立时间

建立状态下,路由器可以发送/接收:Keepalive;更新;与对等方之间的通知消息。

有关BGP的更多信息,请参见Wikipedia。


@tcrosley-来自维基百科,因此不值得一提。
ocodo 2011年

1
好的,+ 1(包括图表)。:)
tcrosley 2011年

我试图将图表的标签固定为BGP,但它不允许我使用-字符数不足:)
Mike Dunlavey

必须有一个更好的方法来绘制此图形。
工作

1
@Job-有点晚了,很抱歉,但是我现在一直认为这是一个太深奥的例子,我认为保险箱,自动售货机等更有用。
ocodo 2012年

7

它们对于建模各种事物很有用。例如,选举周期可以按照(正常政府)-选举称为->(早期竞选)-议会解散->(繁重竞选)-选举->(投票计数)的州来建模)。然后要么(投票计数)-不占多数->(联盟谈判)-达成协议->(正常政府),要么(投票计数)-多数->(正常政府)。我在带有政治子游戏的游戏中实现了此方案的变体。

它们也被用于游戏的其他方面:人工智能通常基于状态。菜单和级别之间的过渡以及死亡或完成级别时的过渡通常由FSM很好地建模。


++很好的例子。
迈克·邓拉维

1
+1 DFM(确定性有限状态机)的好例子,因为路径。
伊万·普赖斯

4

jquery-csv插件中使用的CSV解析器

这是基本的Chomsky Type III语法分析器。

正则表达式标记器用于逐个字符地评估数据。当遇到控制字符时,代码将被传递到switch语句,以便根据启动状态进行进一步评估。将非控制字符进行分组和批量复制,以减少所需的字符串复制操作次数。

标记器:

var tokenizer = /("|,|\n|\r|[^",\r\n]+)/;

第一组匹配项是控制字符:值定界符(“),值分隔符(,)和条目分隔符(换行符的所有变体)。最后一个匹配项处理非控制字符分组。

解析器必须满足10条规则:

  • 规则#1-每行一个条目,每行以换行符结尾
  • 规则#2-省略文件末尾的尾随换行符
  • 规则#3-第一行包含标头数据
  • 规则#4-空格被视为数据,并且条目不应包含结尾逗号
  • 规则#5-行可以或不可以用双引号定界
  • 规则6-包含换行符,双引号和逗号的字段应用双引号引起来
  • 规则#7-如果使用双引号将字段括起来,则必须在字段内出现的双引号前面加上另一个双引号来转义
  • 修改#1-未引用字段可能会也可能会
  • 修改#2-引用的字段可能会也可能不会
  • 修改#3-条目中的最后一个字段可能包含或可能不包含空值

注意:前7条规则直接来自IETF RFC 4180。添加了最后3个以覆盖现代电子表格应用程序(例如Excel,Google Spreadsheet)引入的边缘情况,这些情况默认情况下不会定界(即引用)所有值。我曾尝试将对RFC所做的更改归还给我,但还没有收到对我的询问的回应。

结束了,下面是示意图:

CSV解析器有限状态机

状态:

  1. 条目和/或值的初始状态
  2. 开场报价
  3. 遇到第二个报价
  4. 遇到未报价的值

过渡:

  • 一种。检查引号(1),未引号(3),空值(0),空条目(0)和新条目(0)
  • b。检查第二个报价字符(2)
  • C。检查转义的引号(1),值的结尾(0)和条目的结尾(0)
  • d。检查值的结尾(0)和条目的结尾(0)

注意:实际上缺少状态。从'c'->'b'应该有一行标有状态'1'的行,因为第二个定界符转义表示第一个定界符仍处于打开状态。实际上,最好将其表示为另一个过渡。创造这些是一门艺术,没有单一的正确方法。

注意:它也缺少退出状态,但是在有效数据上,解析器始终在过渡'a'结束,并且由于没有剩余要解析的状态,所以不可能有任何状态。

状态与过渡之间的差异:

状态是有限的,这意味着只能将其推断为意味着一件事。

过渡表示状态之间的流动,因此可能意味着很多事情。

基本上,状态->过渡关系是1-> *(即一对多)。状态定义了“它是什么”,过渡定义了“它的处理方式”。

注意:不用担心状态/转换的应用不是直观的,而是不直观的。在我最终坚持这个概念之前,花了一些时间与我比一个聪明得多的人进行了广泛的交流。

伪代码:

csv = // csv input string

// init all state & data
state = 0
value = ""
entry = []
output = []

endOfValue() {
  entry.push(value)
  value = ""
}

endOfEntry() {
  endOfValue()
  output.push(entry)
  entry = []
}

tokenizer = /("|,|\n|\r|[^",\r\n]+)/gm

// using the match extension of string.replace. string.exec can also be used in a similar manner
csv.replace(tokenizer, function (match) {
  switch(state) {
    case 0:
      if(opening delimiter)
        state = 1
        break
      if(new-line)
        endOfEntry()
        state = 0
        break
      if(un-delimited data)
        value += match
        state = 3
        break
    case 1:
      if(second delimiter encountered)
        state = 2
        break
      if(non-control char data)
        value += match
        state = 1
        break
    case 2:
      if(escaped delimiter)
        state = 1
        break
      if(separator)
        endOfValue()
        state = 0
        break
      if(newline)
        endOfEntry()
        state = 0
        break
    case 3:
      if(separator)
        endOfValue()
        state = 0
        break
      if(newline)
        endOfEntry()
        state = 0
        break
  }
}

注意:这是要点,实际上还有很多需要考虑的地方。例如,错误检查,空值,尾随空白行(即有效)等。

在这种情况下,状态是正则表达式匹配块完成迭代时的条件。过渡表示为case语句。

作为人类,我们必须简化低级操作到更高级别的抽象,但与FSM工作的倾向正在工作的低水平操作。尽管状态和过渡非常容易单独处理,但是固有地很难一次可视化全部内容。我发现一遍又一遍地遵循各个执行路径,直到我可以直觉过渡如何进行为止。就像学习基础数学一样,您将无法从更高层次评估代码,直到低层次的细节开始变得自动化为止。

撇开:如果您看一下实际的实现,则遗漏了许多细节。首先,所有不可能的路径都会引发特定的异常。击中它们应该是不可能的,但是如果有任何损坏,它们绝对会在测试运行程序中触发异常。其次,“合法” CSV数据字符串中允许使用的解析器规则非常宽松,因此处理许多特定边缘情况所需的代码。不管那个事实,这是在所有错误修复,扩展和微调之前用来模拟FSM的过程。

与大多数设计一样,它不是实现的精确表示,而是概述了重要部分。在实践中,实际上从此设计派生了3种不同的解析器功能:特定于CSV的行拆分器,单行解析器和完整的多行解析器。它们都以相似的方式操作,它们处理换行符的方式也有所不同。


1
哇!非常好的贡献。
ocodo 2012年

@Slomojo谢谢,我很乐意分享。我没有上CS的学校,所以我必须自己学习这些东西。在网上很难找到涉及高层CS主题的实际应用程序。我计划最终详细记录解析器算法,以便将来对像我这样的人很有用。这是一个好的开始。
伊万·普赖斯

我也是自学成才的,但是我是30年前开始的,所以现在我已经讲完了CS课程:)我为您和我这样的人张贴了这个问题。我认为当时学习非常低水平的理论要容易得多,这仅仅是因为虽然实际上没有任何互联网,但我们几乎都生活在洞穴中,这是因为更少的干扰和更多的机会在金属附近工作。
ocodo 2012年

3

Java中的简单FSM

int i=0;

while (i<5) {
 switch(i) {
   case 0:
     System.out.println("State 0");
     i=1;
     break;
   case 1:
     System.out.println("State 1");
     i=6;
     break;
   default:
     System.out.println("Error - should not get here");
     break;      
  }

} 

妳去 好的,它并不出色,但是可以说明这个想法。

您经常会在电信产品中找到FSM,因为它们为其他情况提供了简单的解决方案。


3
它们也是词法分析中编译器构造的重要组成部分。
jmq 2011年

@jmquigley,你能补充一个答案吗?
ocodo 2011年

1
我为您添加了一个单独的答案,并带有几个链接。
jmq 2011年

3

我发现思考/建模电梯(电梯)是有限状态机的一个很好的例子。它几乎不需要引入任何内容,但是提供的实现条件却很琐碎。

这些状态例如是在一楼,一楼等,并且将地面移动到二楼,或者将第三层移动到一楼,但当前在3到2楼之间,依此类推。

电梯轿厢中和地板上的按钮效果本身提供了输入,其效果取决于同时按下的按钮和当前状态。

除顶部和底部之外,每个楼层都有两个按钮:一个请求升降机上升,另一个请求下降。


2

好,这是一个例子。假设您要解析一个整数。它会去像dd*这里d是一个整数位。

state0:
    if (!isdigit(*p)) goto error;
    p++;
    goto state1;
state1:
    if (!isdigit(*p)) goto success;
    p++;
    goto state1;

当然,正如@Gary所说,您可以goto通过switch语句和状态变量来伪装这些。请注意,可以将此代码结构化,与原始正则表达式同构:

if (isdigit(*p)){
    p++;
    while(isdigit(*p)){
        p++;
    }
    // success
}
else {
    // error
}

当然,您也可以使用查找表来完成此操作。

有限状态机可以用多种方法制成,许多事物可以描述为有限状态机的实例。对于事物的思考,它不仅仅是一个概念。

铁路网示例

FSM的一个示例是铁路网络。

火车可以在两个轨道之一上行驶,开关数量有限。

连接这些开关的轨道数量有限。

在任何时候,一列火车都在一条轨道上,可以根据单个输入信息,通过一个开关将其发送到另一条轨道。


(我对您的回答进行了编辑,希望您同意。)
ocodo

@Slomojo:很好。看起来不错。
Mike Dunlavey

2

Ruby中的有限状态机:

module Dec_Acts
 def do_next
    @now = @next
    case @now
    when :invite
      choose_round_partner
      @next = :wait
    when :listen
      @next = :respond
    when :respond
      evaluate_invites
      @next = :update_in
    when :wait
      @next = :update_out
    when :update_in, :update_out
      update_edges
      clear_invites
      @next = :exchange
    when :exchange
      update_colors
      clear_invites
      @next = :choose
    when :choose
      reset_variables
      choose_role
    when :done
      @next = :done
    end
  end
end

这就是分布式系统中单个计算节点的行为,它建立了基于链接的通信方案。或多或少。在“图形”形式中,它看起来像这样:

在此处输入图片说明


+1有趣。DGMM是指什么?
伊万·普赖斯

@EvanPlaice它是基于最大匹配的分布式最小加权顶点覆盖算法(DGMM)...缩写略,不要问我G的来源。
ocodo 2012年

@slomojo“ G”表示“通用”,从中得出的顺序算法使用一种称为通用最大匹配的技术。
philosodad

@philosodad我承担了很多,但我不喜欢发布假设。
ocodo


0

实际上,状态机通常用于:

  • 设计目的(为程序中的不同动作建模)
  • 自然语言(语法)解析器
  • 字符串解析

一个示例是状态机,它扫描字符串以查看其语法是否正确。例如,荷兰邮政编码的格式为“ 1234 AB”。第一部分只能包含数字,第二部分只能包含字母。可以编写一个状态机来跟踪它是处于NUMBER状态还是处于LETTER状态,并且如果遇到错误的输入,则将其拒绝。

此受体状态机具有两个状态:数字状态和字母状态。状态机以数字状态启动,并开始读取要检查的字符串字符。如果在任何状态下都遇到无效字符,该函数将返回False值,并拒绝输入为无效。

Python代码:

import string

STATE_NUMERIC = 1
STATE_ALPHA = 2

CHAR_SPACE = " "

def validate_zipcode(s):
cur_state = STATE_NUMERIC

for char in s:
    if cur_state == STATE_NUMERIC:
        if char == CHAR_SPACE:
            cur_state = STATE_ALPHA
        elif char not in string.digits:
            return False
    elif cur_state == STATE_ALPHA:
        if char not in string.letters:
            return False
return True

zipcodes = [
    "3900 AB",
    "45D6 9A",
]

for zipcode in zipcodes:
    print zipcode, validate_zipcode(zipcode)

资料来源: 实践中的(有限)状态机

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.