状态机的设计比较复杂,这里主要是分析最简单的情况,作为入门知识的介绍。
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.
将程序修改一下,即可用于滤除文件中的多余空格。