状态机的设计比较复杂,这里主要是分析最简单的情况,作为入门知识的介绍。
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还是INint 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 0111011101110011110state 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还是1int 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 011101110111001111001111101110state 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,其他为0int 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=1c=o,input=1 counter=2c=w,input=1 counter=3c= ,input=0 找到第1个单词:Howc=a,input=1 counter=1c=r,input=1 counter=2c=e,input=1 counter=3c= ,input=0 找到第2个单词:arec=y,input=1 counter=1c=o,input=1 counter=2c=u,input=1 counter=3c=?,input=0 找到第3个单词:youc= ,input=0 c=F,input=1 counter=1c=i,input=1 counter=2c=n,input=1 counter=3c=e,input=1 counter=4c=!,input=0 找到第4个单词:Finec= ,input=0 c=t,input=1 counter=1c=h,input=1 counter=2c=a,input=1 counter=3c=n,input=1 counter=4c=k,input=1 counter=5c= ,input=0 找到第5个单词:thankc=y,input=1 counter=1c=o,input=1 counter=2c=u,input=1 counter=3c=.,input=0 找到第6个单词:youc= ,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 1100010001110000111000001100000100000state 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.
将程序修改一下,即可用于滤除文件中的多余空格。