讀古今文學網 > 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.
  

將程序修改一下,即可用於濾除文件中的多餘空格。