【剑指Offer-19】正则表达式匹配

问题

请实现一个函数用来匹配包含'. '和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。

示例

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。

输入:
s = "aab"
p = "cab"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

解答1:动态规划

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size(), n = p.size();
        bool dp[m + 1][n + 1]; memset(dp, 0, sizeof(dp));
        dp[0][0] = true;
        auto matches = [&](int i, int j) { // lambda函数
            if (i == 0) return false;
            if (p[j - 1] == '.') return true;
            return s[i - 1] == p[j - 1];
        };
        for (int i = 0; i <= m; i++)
            for (int j = 1; j <= n; j++) {
                if (p[j - 1] != '*') { // 情况1、2
                    if (matches(i, j))
                        dp[i][j] = dp[i - 1][j - 1];
                } else { // 情况3
                    dp[i][j] = dp[i][j - 2]; // 情况3中s[i]与p[j-1]相同的第二种情况,以及情况3中s[i]与p[j-1]不相同的情况
                    if (matches(i, j - 1))
                        dp[i][j] |= dp[i - 1][j]; // 情况3中s[i]与p[j-1]相同的第一种情况,|=表示或运算
                }
            }
        return dp[m][n];
    }
};

重点思路

二维数组dp[i][j]表示待匹配字符串s的前i个字符,与正则匹配字符串p的前j个字符是否匹配。针对本问题的状态转移方程,需要考虑以下3种情况:

  1. 如果p[j]为小写字母,则s[i]必须为相同的小写字母才能匹配成功;
  2. 如果p[j]为.,则必定匹配成功;
  3. 如果p[j]为*,如果p[j-1]与s[i]不相同,则只能把*理解为前一个字符出现0次;如果相同,其匹配过程本质只有两种情况,以下详细讨论。

对于上述第3点,这两种情况为:

  • 匹配s末尾的一个字符,将该字符扔掉,而该组合还可以继续进行匹配;
  • 不匹配字符,将该组合扔掉,不再进行匹配。

综上所述,我们可以总结出以下状态转移方程:

\[\text{dp}[i][j]=\left\{\begin{array}{ll} \text { if }\left(p[j] \neq \text{'}*\text{'}\right) & =\left\{\begin{array}{ll} \text{dp}[i-1][j-1], & \text { matches }(s[i], p[j]) \\ \text { false, } & \text { otherwise } \end{array}\right. \\ \text { otherwise } & =\left\{\begin{array}{ll} \text{dp}[i-1][j] \text { or } \text{dp}[i][j-2], & \operatorname{matches}(s[i], p[j-1]) \\ \text{dp}[i][j-2], & \text { otherwise } \end{array}\right. \end{array}\right. \]

其中or对应的就是第3点中的两种情况,只需要满足任意一个即可,在代码中体现在|=运算符中。

需要特别注意的是本题的动态规划数组的初始化。s与p的空与非空对应以下4种情况:

  1. s空p空,依照题意,匹配成功,返回true;
  2. s非空p空,匹配失败,返回false;
  3. s空p非空,只有当p是.*这样的形式才返回true,否则返回false;
  4. s非空p非空,没啥好说的。

第1种情况在代码第6行被单独定义;第2种情况中,注意第13行的循环,j是从1开始的,则这种情况不会进入循环体,输出dp[m][0]为false;第3种情况,在执行循环时,会进入matches函数,而该函数内定义了i=0的情况,不会出现后面坐标i-1越界的问题(说到越界问题,再提一嘴j-2也不会越界,因为本题默认p的第1个字符不可能是*)。当p为.*这种形式时,最终dp[0][m]会退化到dp[0][0],返回true。

还需要注意的是,dp两个维度都比对应字符串的长度大1(因为要单独定义为空的情况),所以涉及到字符串上的角标取值时,都需要减去1,如第10行代码。

解答2:递归

class Solution {
public:
    bool isMatch(string s, string p) {
        if (p.empty()) return s.empty();
        bool matchFirst = !s.empty() && (p[0] == s[0] || p[0] == '.'); // s和p的首字母匹配
        if (p[1] == '*') return isMatch(s, p.substr(2)) || (matchFirst && isMatch(s.substr(1), p));
        return !s.empty() && (matchFirst && isMatch(s.substr(1), p.substr(1)));
    }
};

重点思路

正则表达式匹配 - 递归求解

  1. 特判,同时也是递归出口,如果p是空串,返回s是否为空串。如果p不为空,保证一定存在p[1](可能是字符串结尾\0);
  2. 假如p[1] == *的话,可以尝试两种情况:情况一是递归比较sp.substr(2);情况二是当s[0]可以匹配p[0]时, 尝试递归比较s.substr(1)p,这里没有必要比较s.substr(1)p.substr(2),因为这种情况已经包含在递归比较s.substr(1)p当中了;
  3. 假如p[1] != *,如果p[0]不匹配s[0],返回false,否则递归判断s.substr(1)p.substr(1)

解答3:非确定性有穷自动机(NFA)

class Nfa {
public:
    void build(string re) { // 建图
        this->re = re;
        n = re.size();
        for (int i = 0; i < n - 1; i++) {
            if (re[i + 1] == '*') {
                graph[i].push_back(i + 1);
                graph[i + 1].push_back(i + 2);
                graph[i + 1].push_back(i);
            }
        }
    }
    bool isMatch(string target) {
        unordered_set<int> marked; // 存储有向图上当前节点能达到的所有下一节点
        dfs(0, marked); // 初始化marked(初始节点能到达的节点)
        for (int i = 0; i < target.size(); i++) { // 遍历待匹配字符串
            vector<int> matched; // matched存储经过匹配后marked中节点能到达的所有下一节点
            for (auto v : marked) {
                if (v == n) continue; // 已到达正则串最后的'\0'节点,但待匹配字符串还没匹配完,丢弃该状态
                if (re[v] == target[i] || re[v] == '.') // 当前节点与待匹配字符相同或者为'.'时,将当前节点的后一个节点添加进matched
                    matched.push_back(v + 1);
            }
            marked.clear();
            for (auto v : matched) // 以每个matched节点为起点,生成marked
                dfs(v, marked);
        }
        for (auto v : marked) // 待匹配字符串遍历结束后,matched节点含正则串的末尾'\0'时,匹配成功
            if (v == n) return true;
        return false;
    }
private:
    int n; // 正则串的size
    string re;
    unordered_map<int, vector<int>> graph;
    void dfs(int cur, unordered_set<int>& marked) {
        marked.insert(cur);
        for (auto v : graph[cur]) {
            if (marked.count(v)) continue;
            dfs(v, marked);
        }
    }
};

class Solution {
public:
    bool isMatch(string s, string p) {
        Nfa nfa;
        nfa.build(p);
        return nfa.isMatch(s);
    }
};

重点思路

具体见代码注释。打算先了解正则表达式、NFA、DFA之间的关系和转换方法,今后有时间打算单独整理一篇文章。上述实现的NFA包括有向图的构建有向图的深度优先搜索

posted @ 2021-02-23 22:55  tmpUser  阅读(55)  评论(0编辑  收藏  举报