关于状态机 finite-state machine

相关资料来自Finite-state machine
可以看做一个抽象的机器,一次只能在一个状态。如果被一个触发事件或者条件所发起,可以从一个状态变到另外一个状态,这个过程叫做转变。一个特定的Finite-state machine(FSM)有有限个状态,每一种转变有一个特定的触发条件。
但是只有有限个状态,所以FSM得能力不如其他的计算模型,例如图灵机Turing machine。
关于状态机 finite-state machine
文章图片



Event-driven finite-state machine 下面的程序就是一个车上的播放器的简单表示,总共两个状态:radio模式和CD模式。 If (mode change) radio to CD back and forth; if (go to next) next preset for radio or next CD track; 用到了枚举和switch语句。

/********************************************************************/ #include /********************************************************************/ typedef enum { ST_RADIO, ST_CD } STATES; typedef enum { EVT_MODE, EVT_NEXT } EVENTS; EVENTS readEventFromMessageQueue(void); /********************************************************************/ int main(void) { /* Default state is radio */ STATES state = ST_RADIO; int stationNumber = 0; int trackNumber = 0; /* Infinite loop */ while(1) { /* Read the next incoming event. Usually this is a blocking function. */ EVENTS event = readEventFromMessageQueue(); /* Switch the state and the event to execute the right transition. */ switch(state) { case ST_RADIO: switch(event) { case EVT_MODE: /* Change the state */ state = ST_CD; break; case EVT_NEXT: /* Increase the station number */ stationNumber++; break; } break; case ST_CD: switch(event) { case EVT_MODE: /* Change the state */ state = ST_RADIO; break; case EVT_NEXT: /* Go to the next track */ trackNumber++; break; } break; } } }


Automata-based programming 这是一种编程规范,这种程序或者它的一部分可以被认作是FSM或者其他的更正式的automaton。 一些关键的性质: 1 程序的执行时间周期被清晰分割成自动机的几个步骤。每一步都是一个代码段(code section),有单一的入口。这种代码段可以使一个函数或其他路径,或仅仅是一个循环体。这个代码段可能分为子段(尽管不需要)基于不同的状态进行执行。 2 不同步骤的程序区段只能通过 一组清楚标示的变量交换信息,这些变量称为状态(state),使用自动机编程的程序不能用其他不显然可见的方式标示状态,例如区域变量的数值、回传地址、目前程序指针的位置等。因此一程序在任二个不同时间下的差异,只有自动机的状态,这些变量,数值不同,其余都相同。
用一个简单例子来说明:从标准输入流中一行一行地读入文本,只输出每一行的第一个单词。 我们首先要略过每一行前面的无效字符,读入第一个单词的字符并输出到单词结束;然后读入并略过这一行的所有字符。如果读到'\n',我们重新进入这个算法,直到EOF。
传统的C实现:
#include int main(void) { int c; while ((c = getchar()) != EOF) { if ((c > 'Z'&&c<'a') || c>'z' || c < 'A')//对输入行的前面的字符进行处理 continue; else { putchar(c); while (((c = getchar()) <= 'z'&&c >= 'a') || (c <= 'Z' && c >= 'A')) putchar(c); //输出第一个单词,跳出循环时候的第一个字符无效 if (c == '\n') { putchar(c); continue; //这里换行,说明只输入一个有效单词,直接进入下一个大循环 } while ((c = getchar()) != EOF&&c != '\n') ; if (c == '\n') { putchar(c); continue; } else if (c == EOF) break; } } }


WIKI上给的版本是
#include int main(void) { int c; do { c = getchar(); while (c == ' ') c = getchar(); while (c != EOF && c != ' ' && c != '\n') { putchar(c); c = getchar(); } putchar('\n'); while (c != EOF && c != '\n') c = getchar(); } while (c != EOF); return 0; }


但是在实验中,这份代码连hsdf^&*(4545这种都认定为单词,有效,可以输出!!!所以我的复杂,但是功能大一点
用到Automata-based style program的编程方法:
#include int main(void) { int c; enum states { BEFORE, INSIDE, AFTER }state; state = BEFORE; while ((c = getchar()) != EOF) { switch (state) { case BEFORE: switch (c) { case ' ':break; case '\n': putchar(c); break; default: putchar(c); state = INSIDE; break; } break; case INSIDE: switch (c) { case ' ': state = AFTER; break; case '\n': putchar(c); break; default: putchar(c); break; } break; case AFTER: switch (c) { case '\n': putchar(c); break; default: break; } } } return 0; }

这种方法相较于WIKI的那种方法,虽然代码长,但是只用了一个读取函数getchar(),而且只用到了一个循环。while循环内的程序就是 自动机的步骤,而循环本身即可重复的运行自动机的程序。

自动机编程中, 不一定要为每一个状态撰写独立的处理程序,而且有时状态是由许多变量组成,无法针对每一个状态规划个别的处理程序 。此想法有时有助于程序的精简, 例如在上述程序中,不论是在哪一个状态,针对换行字符的处理都一様,因此程序可以先处理换行字符,其他输入字符时才依不同状态进行处理 ,简化后变成以下的程序:


#include int main(void) { int c; enum states { BEFORE, INSIDE, AFTER }state; state = BEFORE; while ((c = getchar()) != EOF) { if (c=='\n') { putchar(c); state = BEFORE; } else { switch (state) { case BEFORE: switch (c) { case ' ':break; default: putchar(c); state = INSIDE; break; } break; case INSIDE: switch (c) { case ' ': state = AFTER; break; default: putchar(c); break; } break; //don't need to care about case AFTER } } } return 0; }



独立的自动机步骤程序

上述程序的一个重要特点是自动机步骤的程序区块都只使用区域变量,以下的例子将自动机步骤集成为一个独立的函数 step() ,更可以突显上述的特点:
#include enum states { BEFORE, INSIDE, AFTER }; void step(enum states *, char); int main(void) { int c; enum states state= BEFORE; while ((c = getchar()) != EOF) { step(&state, c); } return 0; } void step(enum states* state, char c) { if (c == '\n') { putchar(c); *state = BEFORE; } else { switch (*state) { case BEFORE: switch (c) { case ' ':break; default: putchar(c); *state = INSIDE; break; } break; case INSIDE: switch (c) { case ' ': *state = AFTER; break; default: putchar(c); break; } break; //don't need to care about case AFTER } } }


此例清楚的呈现自动机编程程序的基本特点:

  1. 各自动机步骤程序的运行时间不互相重叠。
  2. 前一个步骤和下一个步骤之间所交换的数据只有标示为“自动机状态”的变量(此例中为变量state)。

显式的状态转换表

自动机编程可以用显式的状态转换表来表示。以下的程序中的the_table数组即为状态转换表,其列表示三个不同的状态,其每一栏对应输入的字符(从左到右分别是空白、换行字符及其他字符)。
对于每一种可能的状态及输入字符的组合,表中有其对应的新状态及一个决定是否否显示输入字符的旗标。在实务的项目中状态转换表可能更为复杂,例如可能包括所有可能条件组合下需调用的函数指针。
#include enum states { before = 0, inside = 1, after = 2 }; struct branch { unsigned char new_state : 2; unsigned char should_putchar : 1; }; struct branch the_table[3][3] = { /* ' ''\n'others */ /* before */{ { before, 0 }, { before, 1 }, { inside, 1 } }, /* inside */{ { after, 0 }, { before, 1 }, { inside, 1 } }, /* after*/{ { after, 0 }, { before, 1 }, { after, 0 } } }; void step(enum states *state, int c) { int idx2 = (c == ' ') ? 0 : (c == '\n') ? 1 : 2; struct branch *b = &the_table[*state][idx2]; *state = (enum states)(b->new_state); if (b->should_putchar) putchar(c); } int main(void) { int c; enum states state = before; while ((c = getchar()) != EOF) step(&state, c); return 0; }


这个程序真的很(kan)高(bu)明(dong)... 首先,states的三种枚举可以各用0、1、2代替。state初始化为before,读入' '时候,idx2=0;
b是struct branch的指针向量,得到the_table[before][0],也就是b=&the_table[0][0](这里存疑:不是前面有一个去之地符号?答:这是对指针b进行初始化的过程中赋值,可以认为b就是指向 the_table[*state][idx2]={before,0} 的指针); 这时候更新state:b->new_state是0,0在states中是before,所以state=before; 这时候,对b->should_putchar进行判断,值为0,不输出。返回main()。
如果这时候,输入!=' '&&!='\n'的字符,idx2=2,b=&the_table[before][2]即为*b=the_table[0][2],更新为指向{inside,1}的指针。然后state指向的值变为inside(main()函数中的state变量值为inside),b->should_putchar=1,输出字符。
这时候再读入空格,则b=&the_table[inside][0]={after,0}; (这里的after主要处理的是后续字符,无论是什么都只能跳转到after,或者输出后初始化)state变为after,不输出字符
在after状态下,读入空格或者读入others都还是跳转到{after,0},只有读入'\n'跳转到{before,1}即状态初始化,但是输出这个回车字符。
如果,初始状态下输入字符是'\n',idex2=1,b得到的是{before,1}即输出后初始化state。
【关于状态机 finite-state machine】其实,跳转表已经用the_table列出。

    推荐阅读