zoukankan      html  css  js  c++  java
  • 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=

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

  • 相关阅读:
    SpringBoot整合Flyway(数据库版本迁移工具)
    Java并发编程实战 05等待-通知机制和活跃性问题
    Java并发编程实战 04死锁了怎么办?
    Java并发编程实战 03互斥锁 解决原子性问题
    Java并发编程实战 02Java如何解决可见性和有序性问题
    Flutter学习笔记(40)--Timer实现短信验证码获取60s倒计时
    Flutter学习笔记(39)--BottomNavigationBar底部item超过3个只显示icon,不显示title
    Flutter学习笔记(38)--自定义控件之组合控件
    Flutter学习笔记(36)--常用内置动画
    Flutter学习笔记(37)--动画曲线Curves 效果
  • 原文地址:https://www.cnblogs.com/dylan2011/p/2688592.html
Copyright © 2011-2022 走看看