狀態機的設計比較複雜,這裡主要是分析最簡單的情況,作為入門知識的介紹。
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.
將程序修改一下,即可用於濾除文件中的多餘空格。