状态机的设计比较复杂,这里主要是分析最简单的情况,作为入门知识的介绍。
1.状态机的条件错误
【例11.1】下面是统计输入单词的程序,但输出结果不对,请分析它的工作原理并改正错误。
#include <stdio.h> enum { OUT=0 , IN=1 } ; int main () { int count = 0 , state = OUT ; char c ; while ((c = getchar ()) != \'#\' ) { if (c==\' \' || c == \'t\' || c == \'\0\' ) state=OUT ; else if (state == OUT ) { state=IN ; ++count ; } } printf (\"word count :%dn\" , count ); return 0 ; }
程序示范运行如下:
We are here ! Go home ! Right !# word count :4
【解答】先分析最简单的情况,只输入1行文字,遇到“#”结束。
程序使用两种状态来描述,即IN和OUT。它使用状态和状态转换来描述:
(1)初始state=OUT。
(2)当读入空格时,相当于先输入空格,if条件满足,重新执行state=OUT并结束这一次的循环,开始下一次循环。如果继续是空格,则继续这个过程。
(3)由此可见,当处于OUT时,如果读入的是空格,状态不发生转换。一直到读到非空格时,执行else if语句,就置state=IN,计数器加1。
(4)当读入处于IN状态时,读到非空格时,if和else if的条件均不成立,状态不转换,读到空格时,if成立,置state=OUT,即读完一个单词。
(5)重复上述过程。
当需要输入多行时,应该将换行与空格等同看待。但\'\0\'是字符串结束符,换行符是\'n\'。因为缺少换行符,所以把换行后的第1个单词与上一行最后一个单词算作一个单词,就少计数2个,所以输出4。用n替换0即可。运行时再输入相同数据,就会得到正确结果为6。
2.遗漏状态机的状态
【例11.2】下面是增加输入状态统计输入单词的程序,但输出结果不对,请找出并改正错误。
#include <stdio.h> enum { OUT=0 , IN=1 } ; // 根据输入类型,字符返回1 ,其他为0 //input=get_input (char c )决定input 状态OUT 还是IN int get_input (char c ) { if ( c>= \'a\' && c <= \'z\' ) return IN ; if ( c>= \'A\' && c <= \'Z\' ) return IN ; return OUT ; } int main () { int words=0 , state=OUT , input=OUT ; char c ; while ((c = getchar ()) != \'#\' ) { input=get_input (c ); // 起始状态为0 ,无输入,保持state=0 if ((state==OUT )&& (input==OUT )) { state=OUT ; } //state=0 ,input=1 ,置state=1 并计数 else if ((state==OUT )&& (input==IN )) { state=IN ; words++ ; } } printf (\"word count :%dn\" , words ); return 0 ; }
运行错误结果如下:
we are here ! Go home !# word count :1
【解答】本题是与上面的例子的目的相同,都是统计输入文本的单词数。上题使用一个状态变量编程,本题使用两个状态编程。
本题设计一个函数get_input,用来将输入转换为OUT和IN,供输入状态input使用。一个状态量的取值就是2种,但两个状态量的变化却有4种。程序在处理时,只处理两种,所以程序的输出不正确。
假设输入一串文字“How are you?Fine!”,分析一下状态变化规律。为了好对照,改用0和1表示两种状态值。对input而言,约定输入字符为1,输入非字符为0。state也使用相同约定。开始状态两者均为0。
输入 How are you ? Fine ! input 0111011101110011110 state 0111011101110011110
因字母大小不一,所以上下对不齐,就按顺序对应,状态表中把state放在前面。
起始:input=0,state=0。如果开始时使用空格,也与此一样,状态都不发生变化,即
if ((state==OUT )&& (input==OUT )) state=OUT ;
当前状态表0 0(OUT,OUT)
输入:输入有效字符input=1,state变为1,单词计数,即
else if ((state==OUT )&& (input==IN )) { state=IN ; words++ ; }
状态变换对应:0 1(OUT IN)
转换后状态1 1(IN,IN)
输入结束:input=0时表示结束,置state=0。程序里没处理这一项,即
else if ((state==IN )&& (input==OUT )) state=OUT ;
状态变换对应:1 0(IN OUT)
转换后状态0 0(OUT,OUT)
继续输入:保持input和state都为1。程序里也没处理这一项,即
if ((state==IN )&& (input==IN )) state=IN ;
由以上分析可知。需要处理所有可能的状态转换。
// 改正后的程序 #include <stdio.h> enum { OUT=0 , IN=1 } ; // 根据输入类型,字符返回1 ,其他为0 //get_input 函数决定input 状态0 还是1 int get_input (char c ) { if ( c>= \'a\' && c <= \'z\' ) return IN ; if ( c>= \'A\' && c <= \'Z\' ) return IN ; return OUT ; } int main () { int words=0 , state=OUT , input=OUT ; char c ; while ((c = getchar ()) != \'#\' ) { input=get_input (c ); // 起始状态为0 ,无输入,保持state=0 if ((state==OUT )&& (input==OUT )) { state=OUT ; } //state=0 ,input=1 ,置state=1 并计数 else if ((state==OUT )&& (input==IN )) { state=IN ; words++ ; //input=1 ,state 从0 变到1 ,单词起点 } //state=1 ,等待input=0 ,置state=0 ,一个单词结束 else if ((state==IN )&& (input==OUT )) { state=OUT ; } //state=1 ,input=1 ,置state=1 ,继续输入 if ((state==IN )&& (input==IN )) { state=IN ; } } printf (\"word count :%dn\" , words ); return 0 ; }
验证如下:
we are here ! Go home !# word count :5
利用两个状态建立状态转移的方法很有用处。
3.使用状态机的例子
【例11.3】演示使用状态机对数组里的单词计数并输出单词的程序。
【分析】因为要输出单词,所以要使用计数器记录单词有多少字符以便打印。还需要设计一个字符指针跟踪字符串。为了简单,直接使用0和1表示状态。假设字符串的内容,用这个字符串画出input和state的关系,根据这些关系,注意打印是在一个单词结束之后,所以是选寻找打印的位置,然后计数,单词结束后再打印,所以p只是列出对应的打印操作,用“+”表示第1个单词字符计数。
buf How are you ? Fine ! thank you. input 011101110111001111001111101110 state 011101110111001111001111101110 ppp ppp ppp pppp ppppp ppp +++
因为其他情况一样,所以只分析一下打印。打印H,state从0到1,input=1时,将buf的地址赋给指针p(p=&buf[i])。为了方便打印,将单词计数放在单词的结束,这时就可以打印遍历过的字符。即state=1,input变为0,将state置0后,打印单词。
单词的计数是在继续输入时进行的,即state=1,input=1,input继续为1。
有两个变量和4种状态,为了进一步理解它们的操作,加入一些打印信息。完成的程序和运行结果如下所示,注意标点符号不属于单词。
#include <stdio.h> int get_input (char ); int main () { char buf=\"How are you ? Fine ! thank you.\" ; int input=0 , i=0 , state=0 , words=0 ; char c ; char *p=NULL ; // 打印起点 int counter=0 ; // 单词计数 while (1 ) { c=buf[i] ; input=get_input (c ); printf (\"c=%c ,input=%d \" ,c ,input ); if (c==\'\0\' ) break ; if ((state==0 )&& (input==0 )) { state=0 ; } //state=0 ,input=1 ,置state=1 并打印 else if ((state==0 )&& (input==1 )) { state=1 ; p=&buf[i] ; //input=1 ,state 从0 变到1 ,单词起点 } //state=1 ,等待input=0 ,置state=0 ,一个单词结束 // 输出这个单词并置计数器为0 else if ((state==1 )&& (input==0 )) { int j=0 ; state=0 ; words++ ; printf (\" 找到第%d 个单词:\" ,words ); for (j=0 ;j<counter ;j++ ) // 打印单词 printf (\"%c\" ,p[j] ); printf (\"n\" ); counter=0 ; // 单词计数器计数置0 } //state=1 ,input=1 ,置state=1 // 单词计数器计数 if ((state==1 )&& (input==1 )) { state=1 ; counter++ ; // 单词计数器计数 printf (\"counter=%dn\" ,counter ); } i++ ; // 循环变量加1 后返回起点 } // 循环结束 printf (\" 一共找到%d 个单词。n\" ,words ); return 0 ; } // 字符返回1 ,其他为0 int get_input (char c ) { if ( c>= \'a\' && c <= \'z\' ) return 1 ; if ( c>= \'A\' && c <= \'Z\' ) return 1 ; return 0 ; } c=H ,input=1 counter=1 c=o ,input=1 counter=2 c=w ,input=1 counter=3 c= ,input=0 找到第1 个单词:How c=a ,input=1 counter=1 c=r ,input=1 counter=2 c=e ,input=1 counter=3 c= ,input=0 找到第2 个单词:are c=y ,input=1 counter=1 c=o ,input=1 counter=2 c=u ,input=1 counter=3 c= ?,input=0 找到第3 个单词:you c= ,input=0 c=F ,input=1 counter=1 c=i ,input=1 counter=2 c=n ,input=1 counter=3 c=e ,input=1 counter=4 c= !,input=0 找到第4 个单词:Fine c= ,input=0 c=t ,input=1 counter=1 c=h ,input=1 counter=2 c=a ,input=1 counter=3 c=n ,input=1 counter=4 c=k ,input=1 counter=5 c= ,input=0 找到第5 个单词:thank c=y ,input=1 counter=1 c=o ,input=1 counter=2 c=u ,input=1 counter=3 c=. ,input=0 找到第6 个单词:you c= ,input=0 一共找到6 个单词。
这个程序的state和input的状态都是两个,用上面的程序也能去掉空格,但是不能输出标点符号。
【例11.4】修改上述程序使其去掉字符串中的多余空格。
#include <stdio.h> int get_input (char ); int main () { char buf=\"How are you ? Fine ! thank you.\" ; int input=0 , i=0 , state=0 ; char c ; char *p=NULL ; // 打印起点 int counter=0 ; // 单词计数 while (1 ) { c=buf[i] ; input=get_input (c ); if (c==\'\0\' ) break ; if ((state==0 )&& (input==0 )) { state=0 ; } else if ((state==0 )&& (input==1 )) { state=1 ; p=&buf[i] ; //input=1 ,state 从0 变到1 ,单词起点 } else if ((state==1 )&& (input==0 )) { int j=0 ; state=0 ; words++ ; for (j=0 ;j<counter ;j++ ) printf (\"%c\" ,p[j] ); printf (\" \" ); counter=0 ; } if ((state==1 )&& (input==1 )) { state=1 ; counter++ ; } i++ ; } printf (\"n\" ); return 0 ; } int get_input (char c ) { if ( c>= \'a\' && c <= \'z\' ) return 1 ; if ( c>= \'A\' && c <= \'Z\' ) return 1 ; return 0 ; }
程序运行结果如下:
How are you Fine thank you
为了解决这个问题,可以使state和input的状态数目不一样。
【例11.5】使用状态机去除多余的空格的程序。
第1个空格是重要的,状态要发生改变。这里把input空格定义为1,其他应为0。
对state而言,关心的是空格,所以字符对应0。第1个空格为1,第2个空格就必须与之区分,定义为2。同理,第3个空格也应为2。对字符不关心,遇到字符(包括标点符号)回到0状态,只要不是状态2,就都打印出来。
buf \" How are you ? Fine ! thank you.n input 1100010001110000111000001100000100000 state 01200010001220000122000001200000100000 p p pppppppp ppppp pppppp ppppppppppp
对照上述状态,可以列出一个状态跳转表。
0 0-->0 0 1-->1 1 0-->0 1 1-->2 2 0-->0 2 1-->2
根据状态跳转表,编写出如下程序。
#include <stdio.h> int get_input (char ); int main () { char buf=\"How are you ? Fine ! thank you.\" ; int input=0 , i=0 , state=0 ; char c ; char *p=NULL ; // 打印起点 int counter=0 ; // 单词计数 while (1 ) { c=buf[i] ; input=get_input (c ); if (c==\'\0\' ) break ; if ((state==0 )&& (input==0 )) { state=0 ; printf (\"%c\" ,c ); } else if ((state==0 )&& (input==1 )) { state=1 ; printf (\"%c\" ,c ); } else if ((state==1 )&& (input==0 )) { state=0 ; printf (\"%c\" ,c ); } else if ((state==1 )&& (input==1 )) { state=2 ; //nothing } else if ((state==2 )&& (input==0 )) { state=0 ; printf (\"%c\" ,c ); } if ((state==2 )&& (input==1 )) { state=2 ; //no out ; } i++ ; } printf (\"n\" ); return 0 ; } int get_input (char c ) { if ( c== \' \' ) return 1 ; return 0 ; }
程序运行结果如下:
How are you ? Fine ! thank you.
将程序修改一下,即可用于滤除文件中的多余空格。