映月读书网 > C语言解惑 > 第11章 状态机 >

第11章 状态机

状态机的设计比较复杂,这里主要是分析最简单的情况,作为入门知识的介绍。

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.
  

将程序修改一下,即可用于滤除文件中的多余空格。