Leetcode-10.正则表达式匹配(暴力 或 记忆暴力)

一、题目背景

我们要实现一个简化版的正则表达式匹配函数 isMatch(s, p),其中:

  • s:文本串(普通字符串)

  • p:模式串,只包含:

    • 普通字符(例如 'a', 'b' 等)

    • 特殊字符 '.':匹配任意单个字符

    • 特殊组合 '*':匹配 零个或多个 前一个字符(例如 a*, b*, .*

要求:
必须匹配整个字符串 s,不能只匹配前半段或中间一段。


二、暴力递归思路(会超时)

最直接的想法是:

用递归从左往右匹配 sp,每次只看当前的第一个字符,处理两种情况:

  1. 当前首字符是否匹配(firstMatch)

    cpp 复制代码
    bool fristVail = (!s.empty() && (s[0] == p[0] || p[0] == '.'));

    也就是:

    • s 不能为空

    • 并且 s[0]p[0] 相等,或者 p[0] == '.'

  2. 第二个字符是否是 '*'

    • 如果 p[1] == '*',说明当前模式是 X*,可以有两种选择:

      • X* 当成「出现 0 次」:直接跳过 p 的前两个字符 → isMatch(s, p.substr(2))

      • X* 当成「吃掉一个字符」:前提是首字符匹配 → fristVail && isMatch(s.substr(1), p)

    • 否则就是普通情况:

      首字符必须匹配,然后同时往后挪一格:

      cpp 复制代码
      fristVail && isMatch(s.substr(1), p.substr(1));

这个写法逻辑是对的,但会有大量重复计算 ,在一些构造出来的极端 case 下会 超时


三、优化方向:记忆化搜索(递归 + 缓存)

暴力递归会 TLE 的根本原因是:

同一个子问题(同一段 s[i:]p[j:])会被重复计算很多次。

所以我们引入一个二维数组 memo 来做记忆化:

  • memo[i][j] 表示:
    s[i:]p[j:] 是否匹配的结果

  • 取值约定:

    • -1:还没计算过

    • 0:不匹配(false)

    • 1:匹配(true)

这样,每当我们递归到 dfs(s, p, i, j) 时:

  1. 先看 memo[i][j] 里有没有记录

  2. 有的话直接返回,不再重复递归

  3. 没有的话正常计算一次,并把结果存入 memo

这样,整个算法的时间复杂度就从「指数级」降到了 O(|s| × |p|)


四、核心函数:dfs 的含义与逻辑

1. 函数定义

cpp 复制代码
bool dfs(const string& s, const string& p, int i, int j)

表示:

当前我们要判断:s 的第 i 个字符开始的后缀 s[i:],是否能匹配模式串从第 j 个字符开始的后缀 p[j:]

也就是把原来的 isMatch(s, p)

变成了带下标版本的 dfs(s, p, i, j)


2. 记忆化剪枝

cpp 复制代码
if (memo[i][j] != -1) return memo[i][j];

如果这个状态之前计算过,直接返回结果。


3. 递归边界:模式串 p 用完了

cpp 复制代码
if (j == p.size()) {  // p 用完了
    ans = (i == s.size());
}
  • 如果 p 也用完了,那么只有在 s 也用完的情况下才是匹配成功

  • 如果 s 没用完,那就说明还有残留字符,匹配失败

这其实对应原来暴力版里的:

cpp 复制代码
if (p.empty()) return s.empty();

只是这里改成了「用下标表示空串」:

  • j == p.size() 表示 p 剩余部分为空

  • i == s.size() 表示 s 剩余部分为空


4. 一般情况:模式串还没用完

如果 p 还没用完,就进入 else 分支:

cpp 复制代码
bool firstMatch = (i < s.size() &&
                  (s[i] == p[j] || p[j] == '.'));

firstMatch 表示当前这一个字符位置上是否能匹配:

  • i < s.size():s 不能越界

  • s[i] == p[j]:同字符

  • 或者 p[j] == '.':点号可以匹配任意一个字符


5. 处理 '*' 的情况

如果 p[j+1] 存在且为 '*'

cpp 复制代码
if (j + 1 < p.size() && p[j + 1] == '*') {
    ans = dfs(s, p, i, j + 2) ||
          (firstMatch && dfs(s, p, i + 1, j));
}

结合含义解释:

  • p[j] 是某个字符 X

  • p[j+1]'*'

  • 整体 p[j] p[j+1] 组成 X*

X* 有两种用法:

① 把 X* 当成「匹配 0 次」

直接跳过 X*,即从 p[j+2] 开始继续匹配:

cpp 复制代码
dfs(s, p, i, j + 2);

此时 s 不动,因为我们选择让 X* 不吃任何字符。


② 把 X* 当成「匹配 ≥1 次」

前提是 firstMatch == true,也就是当前字符可以被 X 匹配。

那么:

  • s 往后移动一格(被 X 吃掉了一个字符)

  • p 仍停在 j,因为 X* 可能继续多次匹配

cpp 复制代码
firstMatch && dfs(s, p, i + 1, j);

这两种情况只要有一种为真,整体就为真,所以用 || 连接。


6. 普通情况(不含 '*'

如果 p[j+1] 不是 '*',那当前就只能是普通字符或 '.' 匹配:

cpp 复制代码
ans = firstMatch && dfs(s, p, i + 1, j + 1);

解读:

  • 当前字符必须先能匹配

  • 然后递归匹配后面的子串:s[i+1:]p[j+1:]


7. 把结果写入 memo

cpp 复制代码
memo[i][j] = ans;
return ans;

避免以后再遇到同样的 (i, j) 时重复计算。


五、对外接口:isMatch

isMatch 只是一个包装函数,负责:

  1. 初始化 memo 数组

  2. (0, 0) 开始匹配

cpp 复制代码
bool isMatch(string s, string p) {
    memo.assign(s.size() + 1, vector<int>(p.size() + 1, -1));
    return dfs(s, p, 0, 0);
}

这里的 s.size() + 1 / p.size() + 1 是因为:

  • i 的取值范围是 [0, s.size()](包含 s.size(),表示空后缀)

  • j 的取值范围是 [0, p.size()](同理)

所以必须多开一格来存 i == s.size()j == p.size() 这些"空串状态"。


六、时间复杂度与优势

有了记忆化之后,每个 (i, j) 状态至多计算一次:

  • 状态数量:(s.size() + 1) * (p.size() + 1)

  • 每个状态的转移是 O(1)

  • 整体时间复杂度:O(|s| × |p|)

  • 空间复杂度同样是 O(|s| × |p|)

相比较原始的暴力递归(指数级),这个解法在 LeetCode 上是完全可以通过的,不会超时。


七、小结

  1. 暴力版本 isMatch(s, p) 核心思想没错,但存在大量重复子问题 → 超时。

  2. 优化思路是:把函数改写成带下标的 dfs(s, p, i, j),并用 memo[i][j] 记忆结果。

  3. 关键点在于正确处理:

    • p 用完:j == p.size()

    • X* 两种分支:

      • 跳过 X*dfs(s, p, i, j + 2)

      • X* 吃掉一个字符:firstMatch && dfs(s, p, i + 1, j)

  4. 外部接口 isMatch 只负责初始化 memo 并从 (0, 0) 开始搜索。

相关推荐
@小白鸽1 小时前
1.3海量数据去重的Hash与BloomFilter
算法·哈希算法
小年糕是糕手1 小时前
【C++】类和对象(四) -- 取地址运算符重载、构造函数plus
c语言·开发语言·数据结构·c++·算法·leetcode·蓝桥杯
sin_hielo1 小时前
leetcode 3625
数据结构·算法·leetcode
不能只会打代码1 小时前
力扣--3625. 统计梯形的数目 II 代码解析(Java,详解附注释附图)
算法·leetcode·职场和发展·力扣
练习时长一年1 小时前
LeetCode热题100(岛屿数量)
算法·leetcode·职场和发展
LXS_3571 小时前
Day 15 C++之文件操作
开发语言·c++·学习方法·改行学it
无限进步_1 小时前
基于单向链表的C语言通讯录实现分析
c语言·开发语言·数据结构·c++·算法·链表·visual studio
老鱼说AI1 小时前
算法初级教学第四步:栈与队列
网络·数据结构·python·算法·链表
客梦1 小时前
数据结构核心内容
数据结构·笔记