有限状态机

转载自公众号:树屋编程

在编程的时候,经常需要根据一个当前状态和各种可能的输入来实现不同的逻辑以及维护状态的变化,一般用if…else或者swith…case来实现,这其实就是状态机的思想。掌握一些关于状态机的理论知识和工具,能够更好更快地写出状态机的代码,尤其是当状态和输入较多的时候。本文从简单的例子开始,介绍状态机的一些知识,然后再介绍各种不同的C++实现。

先来看一个非常简单的面试题怎么用状态机来实现:

有一个ASCII字符串,其中包含数字和非数字,请找出其中所有的连续的数字.比如说对于字符串:
abc123FGX#@AD5LKYO0936sda*xc342s&
找到所有连续的数字如下:
123
5
0936
342

编程逐个遍历题目所需要处理的字符串,每次处理一个字符,字符分为两种:数字(number)和非数字(other),当前状态也可以分为两种:「处在记录数字的状态中(NUMBER)」和「连续的其它非数字状态(OTHER)」。在写代码之前,可以用「状态转移表」来分析每种状态对应每个字符的处理逻辑。表格如下所示,用行和列分别表示所有的状态和对应的输入类型,有N种状态和M种输入的状态机就绘制成一个NxM的表格,然后在每一格中填写对应的处理的逻辑。

图片

每一格的处理逻辑用「新状态/动作行为」的格式来填写,「新状态」表示当前状态遇到当前输入时,当前状态应该被更改为的目标状态,「动作行为」表示在状态改变的同时需要做的一些程序逻辑,例如上面表格中的「记录数字开始位置」和「拷贝保存数字串」。

「状态转移表」的好处是要求将每个格子都填满,不会遗漏。将表格全部填满以后,只要把表格的内容翻译成程序代码即可。对应的C++代码如下:

#include <vector>
#include <string>
#include <iostream>
#include <cctype> // std::isdigit

using namespace std;

// 找出连续的数字,通过一个vector返回,题目并不要求转换成int类型,直接用string即可。
vector<string> FindNumbers(const string& str) {
    vector<string> ret;
    // 定义状态及初始状态
    enum State {OTHER, NUMBER} state = OTHER;
    size_t num_begin; // 当前解析到的连续数字的起始位置
    // 逐个遍历每个字符
    for(size_t i = 0; i < str.length(); ++i) {
        if(isdigit(str[i])) { // 处理数字
            switch(state) {
            case OTHER:
                state = NUMBER;
                num_begin = i;
                break;
            case NUMBER:
                // 什么也不用做
                break;
            }

            // 字符串以数字结尾时,需要加上这行判断,后面再解释。
            if(i == str.length() - 1) {
                ret.emplace_back(str, num_begin, i + 1 - num_begin);
            }
        } else { // 处理非数字
            switch(state) {
            case OTHER:
                // 什么也不用做
                break;
            case NUMBER:
                state = OTHER;
                ret.emplace_back(str, num_begin, i - num_begin);
                break;
            }
        }
    }
    return ret;
}

int main() {
    auto v = FindNumbers("abc123FGX#@AD5LKYO0936sda*xc342s&");
    for(string& s: v) {
        cout<<s<<"\n";
    }
}

这道题作为笔试题的第一题是很适合的,需要注意的一个地方就是当字符串以数字结尾时,最后的一串数字不能忘了,上面代码的第28行判断就是起这个作用。

介绍完上面的简单例子,下面来看看状态机的相关概念:有限状态机(Finite state machine)简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

状态机主要有以下几个要素:

  • 状态(state)

    存储关于过去的信息,就是说:它反映从系统开始到现在时刻的输入变化;

  • 事件(event)

    可能会引发状态发生改变的对系统有意义的事情。在前面的例子中,字符串中每个字符的值为不同的事件;在UI编程中,事件是鼠标和键盘的输入;在网络服务器编程中,事件是来自网络上的数据和服务器内部的定时器。

  • 监护条件(guard)

    状态机对外部消息进行响应时,除了需要判断当前的状态,还需要判断跟这个状态相关的一些条件是否成立。这种判断称为 Guard(监护条件)。Guard 通过允许或者禁止某些操作来影响状态机的行为。

  • 动作(action)

    是在给定时刻要进行的活动的描述,在前面的例子中,「记录数字开始位置」和「拷贝保存数字串」都分别是一个动作。

  • 转移(transition)

    指示状态变更,是状态对事件的反应,事件可能是来自外部或内部,转移还可会伴随着动作。

状态机的表达方式也有好几种,前面介绍了「状态转移表」,下面再来介绍一种「UML状态图」(简称状态图),在状态图中,用圆角矩形表示一个状态、实心圆点表示初始状态、同心圆表示结束状态,以一个简单的游戏AI为例:

图片

在上图中,状态机的五种元素都有对应的符号表示,分别对应为:

图片

有了状态图这个工具,就可以解决更加复杂的状态机的问题了。当然也是先画图再实现为代码。

状态机在C++中除了用简单的if…else…/switch…case…来实现以外,还有很多其它的实现方法。设计模式中有一种「状态模式」,用面向对象的方法实现状态机;C++17增加了std::variant类型,可以对状态进行更好的封装;C++的模板也提供了很多种不同的方案,boost::Statechar/MSM/SML都是相关的实现例子;C++20中增加了协程(coroutine),在某些情况下可以简化状态机的实现,尤其是在异步的状态下,更是可以大大地简化程序逻辑。


例题二:
LeetCode8
在这里插入图片描述

步骤:

  1. 列出状态转移表:横表头表示输入字符,纵表头表示当前状态。每一格的处理逻辑用「新状态/动作行为」的格式来填写。
  2. 将状态转移表翻译成代码。
’ ’+/-numberother
startstartsigned/记录符号in_number/计算end
signedendendin_number/计算end
in_numberendendin_number/计算end
endendendendend
class DFA {
    enum State { START, SIGNED, IN_NUMBER, END };
    using ll = long long;
    State state = START;
    int sign = 1;
    unordered_map<State, vector<State>> table = {
        {START, {START, SIGNED, IN_NUMBER, END}},
        {SIGNED, {END, END, IN_NUMBER, END}},
        {IN_NUMBER, {END, END, IN_NUMBER, END}},
        {END, {END, END, END, END}}};
    int get_col(char c) {
        if (c == ' ')
            return 0;
        else if (c == '+' || c == '-')
            return 1;
        else if (isdigit(c))
            return 2;
        return 3;
    }

   public:
    ll res = 0;
    void input(char c) {
        state = table[state][get_col(c)];
        if (state == SIGNED) {
            sign = c == '+' ? 1 : -1;
        } else if (state == IN_NUMBER) {
            if (sign == 1)
                res = min((ll)INT_MAX, res * 10 + c - '0');
            else if (sign == -1)
                res = max((ll)INT_MIN, res * 10 - (c - '0'));
        }
    }
};

class Solution {
   public:
    int myAtoi(string s) {
        DFA dfa;
        for (char c : s) {
            dfa.input(c);
            if (dfa.res == INT_MAX || dfa.res == INT_MIN)
                return dfa.res;
        }
        return dfa.res;
    }
};

例题3:
考虑终止状态的状态机

在这里插入图片描述
状态转移图
在这里插入图片描述

class Solution {
public:
    enum State {
        STATE_INITIAL,
        STATE_INT_SIGN,
        STATE_INTEGER,
        STATE_POINT,
        STATE_POINT_WITHOUT_INT,
        STATE_FRACTION,
        STATE_EXP,
        STATE_EXP_SIGN,
        STATE_EXP_NUMBER,
        STATE_END,
    };

    enum CharType {
        CHAR_NUMBER,
        CHAR_EXP,
        CHAR_POINT,
        CHAR_SIGN,
        CHAR_SPACE,
        CHAR_ILLEGAL,
    };

    CharType toCharType(char ch) {
        if (ch >= '0' && ch <= '9') {
            return CHAR_NUMBER;
        } else if (ch == 'e' || ch == 'E') {
            return CHAR_EXP;
        } else if (ch == '.') {
            return CHAR_POINT;
        } else if (ch == '+' || ch == '-') {
            return CHAR_SIGN;
        } else if (ch == ' ') {
            return CHAR_SPACE;
        } else {
            return CHAR_ILLEGAL;
        }
    }

    bool isNumber(string s) {
        unordered_map<State, unordered_map<CharType, State>> transfer{
            {
                STATE_INITIAL, {
                    {CHAR_SPACE, STATE_INITIAL},
                    {CHAR_NUMBER, STATE_INTEGER},
                    {CHAR_POINT, STATE_POINT_WITHOUT_INT},
                    {CHAR_SIGN, STATE_INT_SIGN},
                }
            }, {
                STATE_INT_SIGN, {
                    {CHAR_NUMBER, STATE_INTEGER},
                    {CHAR_POINT, STATE_POINT_WITHOUT_INT},
                }
            }, {
                STATE_INTEGER, {
                    {CHAR_NUMBER, STATE_INTEGER},
                    {CHAR_EXP, STATE_EXP},
                    {CHAR_POINT, STATE_POINT},
                    {CHAR_SPACE, STATE_END},
                }
            }, {
                STATE_POINT, {
                    {CHAR_NUMBER, STATE_FRACTION},
                    {CHAR_EXP, STATE_EXP},
                    {CHAR_SPACE, STATE_END},
                }
            }, {
                STATE_POINT_WITHOUT_INT, {
                    {CHAR_NUMBER, STATE_FRACTION},
                }
            }, {
                STATE_FRACTION,
                {
                    {CHAR_NUMBER, STATE_FRACTION},
                    {CHAR_EXP, STATE_EXP},
                    {CHAR_SPACE, STATE_END},
                }
            }, {
                STATE_EXP,
                {
                    {CHAR_NUMBER, STATE_EXP_NUMBER},
                    {CHAR_SIGN, STATE_EXP_SIGN},
                }
            }, {
                STATE_EXP_SIGN, {
                    {CHAR_NUMBER, STATE_EXP_NUMBER},
                }
            }, {
                STATE_EXP_NUMBER, {
                    {CHAR_NUMBER, STATE_EXP_NUMBER},
                    {CHAR_SPACE, STATE_END},
                }
            }, {
                STATE_END, {
                    {CHAR_SPACE, STATE_END},
                }
            }
        };

        int len = s.length();
        State st = STATE_INITIAL;

        for (int i = 0; i < len; i++) {
            CharType typ = toCharType(s[i]);
            if (transfer[st].find(typ) == transfer[st].end()) {
                return false;
            } else {
                st = transfer[st][typ];
            }
        }
        return st == STATE_INTEGER || st == STATE_POINT || st == STATE_FRACTION || st == STATE_EXP_NUMBER || st == STATE_END;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/biao-shi-shu-zhi-de-zi-fu-chuan-lcof/solution/biao-shi-shu-zhi-de-zi-fu-chuan-by-leetcode-soluti/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 4
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值