FSM有限状态机运用分析系列一 —— 文本处理二

  • 用有穷状态机解一道面试题。

刚毕业的时候,我到一家外企面试,面试题里有这样一道题:

统计一篇英文文章里的单词个数。

有多种方法可以解这道题,这里我们选择用有穷状态机来解,做法如下:

先把这篇英文文章读入到一个缓冲区里,让一个指针从缓冲区的头部一直移到缓冲区的尾部,指针会处于两种状态:“单词内”或“单词外”,加上后面提到的初始状态和接受状态,就是有穷状态机的状态集。缓冲区中的字符集合就是有穷状态机的字母表。

如果当前状态为“单词内”,移到指针时,指针指向的字符是非单词字符(如标点和空格),那状态会从“单词内”转换到“单词外”。如果当前状态为“单词外”, 移到指针时,指针指向的字符是单词字符(如字母),那状态会从“单词外”转换到“单词内”。这些转换规则就是状态转换函数。

指针指向缓冲区的头部时是初始状态。

指针指向缓冲区的尾部时是接受状态。

每次当状态从“单词内”转换到“单词外”时,单词计数增加一。
这个有穷状态机的图形表示如下:

下面我们看看程序怎么写:

 1 int count_word(const char* text)
 2 {
 3   /*定义各种状态,我们不关心接受状态,这里可以不用定义。*/
 4   enum _State
 5   {
 6      STAT_INIT,
 7      STAT_IN_WORD,
 8      STAT_OUT_WORD,
 9   }state = STAT_INIT;
10 
11   int count = 0;
12   const char* p = text;
13 
14 /*在一个循环中,指针从缓冲区头移动缓冲区尾*/
15   for(p = text; *p != '/0'; p++)
16   {
17        switch(state)
18        {
19               case STAT_INIT:
20               {
21                      if(IS_WORD_CHAR(*p))
22                      {
23                             /*指针指向单词字符,状态转换为单词内*/
24                             state = STAT_IN_WORD;
25                      }
26                      else
27                      {
28                             /*指针指向非单词字符,状态转换为单词外*/
29                             state = STAT_OUT_WORD;
30                      }
31                      break;
32               }
33               case STAT_IN_WORD:
34               {
35                      if(!IS_WORD_CHAR(*p))
36                      {
37                             /*指针指向非单词字符,状态转换为单词外,增加单词计数*/
38                             count++;
39                             state = STAT_OUT_WORD;
40                      }
41                      break;
42                 }
43             case STAT_OUT_WORD:
44             {
45                   if(IS_WORD_CHAR(*p))
46                   {
47                       /*指针指向单词字符,状态转换为单词内*/
48                       state = STAT_IN_WORD;
49                   }
50                   break;
51             }
52             default:break;
53         }
54     }
55 
56     if(state == STAT_IN_WORD)
57     {
58         /*如果由单词内进入接受状态,增加单词计数*/
59         count++;
60     }
61 
62     return count;
63 }

用状态机来解这道题目,思路清晰,程序简单,不易出错。

这道题目只是为了展示一些奇技淫巧,还是有一些实际用处呢?回答这个问题之前,我们先对上面的程序做点扩展,不只是统计单词的个数,而且要分离出里面的每个单词。

 1 int word_segmentation(const char* text, OnWordFunc on_word, void* ctx)
 2 {
 3     enum _State
 4     {
 5       STAT_INIT,
 6       STAT_IN_WORD,
 7       STAT_OUT_WORD,
 8     }state = STAT_INIT;
 9 
10     int count = 0;
11     char* copy_text = strdup(text);
12     char* p = copy_text;
13     char* word = copy_text;
14 
15     for(p = copy_text; *p != '/0'; p++)
16     {
17         switch(state)
18         {
19             case STAT_INIT:
20             {
21                 if(IS_WORD_CHAR(*p))
22                 {
23                     word = p;
24                     state = STAT_IN_WORD;
25                 }
26                 break;
27             }
28             case STAT_IN_WORD:
29             {
30                 if(!IS_WORD_CHAR(*p))
31                 {
32                     count++;
33                     *p = '/0';
34                     on_word(ctx, word);
35                     state = STAT_OUT_WORD;
36                 }
37                 break;
38             }
39             case STAT_OUT_WORD:
40             {
41                 if(IS_WORD_CHAR(*p))
42                 {
43                     word = p;
44                     state = STAT_IN_WORD;
45                 }
46                 break;
47             }
48             default:break;
49         }
50     }
51 
52     if(state == STAT_IN_WORD)
53     {
54         count++;
55         on_word(ctx, word);
56     }
57 
58     free(copy_text);
59 
60     return count;
61 }

 

状态机不变,只是在状态转换时,做是事情不一样。这里从“单词内”转换到其它状态时,增加单词计数,并分离出当前的单词。至于拿分离出的单词来做什么,由传入的回调函数决定,比如可以用来统计每个单词出现的频率。

但如果讨论还是限于英文文章,这个程序的意义仍然不大,现在来做进一步扩展。我们考虑的文本不再是英文文章,而是一些文本数据,这些数据由一些分隔符分开,我们把数据称为token,现在我们要把这些token分离出来。

 1 typedef void (*OnTokenFunc)(void* ctx, int index, const char* token);
 2 
 3 #define IS_DELIM(c) (strchr(delims, c) != NULL)
 4 int parse_token(const char* text, const char* delims, OnTokenFunc on_token, void* ctx)
 5 {
 6     enum _State
 7     {
 8         STAT_INIT,
 9         STAT_IN,
10         STAT_OUT,
11     }state = STAT_INIT;
12 
13     int count = 0;
14     char* copy_text = strdup(text);
15     char* p = copy_text;
16     char* token = copy_text;
17 
18     for(p = copy_text; *p != '/0'; p++)
19     {
20         switch(state)
21         {
22             case STAT_INIT:
23             case STAT_OUT:
24             {
25                 if(!IS_DELIM(*p))
26                 {
27                     token = p;
28                     state = STAT_IN;
29                 }
30                 break;
31             }
32             case STAT_IN:
33             {
34                 if(IS_DELIM(*p))
35                 {
36                     *p = '/0';
37                     on_token(ctx, count++, token);
38                     state = STAT_OUT;
39                 }
40                 break;
41             }
42             default:break;
43         }
44     }
45 
46     if(state == STAT_IN)
47     {
48         on_token(ctx, count++, token);
49     }
50 
51     on_token(ctx, -1, NULL);
52     free(copy_text);
53 
54     return count;
55 }

用分隔符分隔的文本数据有很多,如:

环境PATH,它由‘:’分开的多个路径组成。如:
/usr/lib/qt-3.3/bin:/usr/kerberos/bin:/backup/tools/jdk1.5.0_18/bin/:/usr/lib/ccache:/usr/local/bin:/bin:/usr/bin:/home/lixianjing/bin

文件名,它由‘/’分开的路径组成。如:
/usr/lib/qt-3.3/bin

URL中的参数,它‘&’分开的多个key/value对组成。
hl=zh-CN&q=limodev&btnG=Google+搜索&meta=&aq=f&oq=

所有这些数据都可以用上面的函数处理,所以这个小函数是颇具实用价值的。

posted on 2012-09-17 13:31  竞击  阅读(786)  评论(0编辑  收藏  举报