正则表达式匹配

正则表达式匹配 Java 题解:动态规划彻底讲清 .*

给定字符串 s 和模式 p,实现支持 .* 的完整匹配。本文用二维动态规划拆解状态定义、空串初始化和星号的两种转移,并附可直接使用的 Java 答案、测试代码和结果。

一、题目规则

  • . 匹配任意一个字符;
  • * 匹配零个或多个前面的元素;
  • 匹配必须覆盖整个字符串,而不是只找到其中一段。
text 复制代码
s = "aa", p = "a"   -> false
s = "aa", p = "a*"  -> true
s = "ab", p = ".*"  -> true

注意:* 不能独立使用,它修饰紧邻它前面的元素。a* 可以表示空串、aaaaaa 等;.* 可以表示任意长度的字符序列。

二、为什么适合动态规划

遇到 * 时有两个选择:

  1. 让前一个元素出现零次,直接跳过"前一个元素 + *";
  2. 当前字符匹配时,让前一个元素再出现一次,消费字符串中的一个字符,但模式位置不变。

这些选择会产生重复子问题,适合使用动态规划保存结果。

三、状态定义

定义:

text 复制代码
dp[i][j]:s 的前 i 个字符,是否能被 p 的前 j 个字符完整匹配。

最终答案为:

java 复制代码
dp[s.length()][p.length()]

空字符串和空模式可以匹配,因此:

java 复制代码
dp[0][0] = true;

四、普通字符和点号

如果当前模式字符不是 *,只有当前字符相互匹配时,才能继承左上角状态:

java 复制代码
boolean matches = p.charAt(j - 1) == '.'
        || p.charAt(j - 1) == s.charAt(i - 1);

if (matches) {
    dp[i][j] = dp[i - 1][j - 1];
}

a 只能匹配 a,而 . 可以匹配任意一个字符,但不能匹配空字符。

五、星号的两种转移

假设当前模式字符为 *,它修饰 p[j - 2]

1. 匹配零次

把前一个元素和 * 一起忽略:

java 复制代码
dp[i][j] = dp[i][j - 2];

例如 s = "b"p = "a*b",其中 a* 匹配零次。

2. 匹配一次或多次

如果 p[j - 2] 能匹配当前字符串字符,则消费一个字符串字符,但继续保留当前模式:

java 复制代码
if (p.charAt(j - 2) == '.'
        || p.charAt(j - 2) == s.charAt(i - 1)) {
    dp[i][j] = dp[i][j] || dp[i - 1][j];
}

这里使用 dp[i - 1][j],是因为 * 仍可能继续匹配更多字符。

六、空字符串初始化

模式即使非空,也可能匹配空字符串,例如:

text 复制代码
"a*"
"a*b*"
".*"

因此需要初始化第一行:

java 复制代码
for (int j = 2; j <= p.length(); j++) {
    if (p.charAt(j - 1) == '*') {
        dp[0][j] = dp[0][j - 2];
    }
}

七、可直接使用的 Java 答案

java 复制代码
class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true;

        for (int j = 2; j <= n; j++) {
            if (p.charAt(j - 1) == '*') {
                dp[0][j] = dp[0][j - 2];
            }
        }

        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                char patternChar = p.charAt(j - 1);

                if (patternChar == '*') {
                    dp[i][j] = dp[i][j - 2];

                    char previous = p.charAt(j - 2);
                    if (previous == '.' || previous == s.charAt(i - 1)) {
                        dp[i][j] = dp[i][j] || dp[i - 1][j];
                    }
                } else if (patternChar == '.'
                        || patternChar == s.charAt(i - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
            }
        }

        return dp[m][n];
    }
}

题目通常保证模式合法,即 * 不会出现在首位,也不会脱离前一个元素。若方法用于通用接口,应先验证模式格式,避免访问 j - 2 时越界。

八、示例推演:aaa*

当处理第二个 a 时:

  • * 匹配零次无法覆盖 aa
  • a 与当前字符匹配;
  • 查看 dp[1][2],表示前一个 a 已经被 a* 匹配;
  • 因此 dp[2][2] = true

这正是 dp[i - 1][j] 表示"继续重复"的含义。

九、复杂度分析

设字符串长度为 m,模式长度为 n

  • 时间复杂度:O(mn)
  • 空间复杂度:O(mn)

可以使用滚动数组将空间优化为 O(n),但二维表更容易理解和验证状态转移。

十、完整测试代码

java 复制代码
public class RegexMatchingTest {
    public static boolean isMatch(String s, String p) {
        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true;

        for (int j = 2; j <= n; j++) {
            if (p.charAt(j - 1) == '*') {
                dp[0][j] = dp[0][j - 2];
            }
        }

        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                char pc = p.charAt(j - 1);
                if (pc == '*') {
                    dp[i][j] = dp[i][j - 2];
                    char previous = p.charAt(j - 2);
                    if (previous == '.' || previous == s.charAt(i - 1)) {
                        dp[i][j] |= dp[i - 1][j];
                    }
                } else if (pc == '.' || pc == s.charAt(i - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
            }
        }
        return dp[m][n];
    }

    private static void test(int n, String s, String p, boolean expected) {
        boolean actual = isMatch(s, p);
        System.out.println("测试 " + n + ": "
                + (actual == expected ? "PASS" : "FAIL")
                + " | actual=" + actual);
    }

    public static void main(String[] args) {
        test(1, "aa", "a", false);
        test(2, "aa", "a*", true);
        test(3, "ab", ".*", true);
        test(4, "aab", "c*a*b", true);
        test(5, "mississippi", "mis*is*p*.", false);
        test(6, "", "a*", true);
        test(7, "", ".", false);
        test(8, "aaa", "ab*a*c*a", true);
    }
}

十一、测试结果

text 复制代码
测试 1:PASS | false
测试 2:PASS | true
测试 3:PASS | true
测试 4:PASS | true
测试 5:PASS | false
测试 6:PASS | true
测试 7:PASS | false
测试 8:PASS | true

十二、常见错误

1. 把 * 当成独立通配符

* 只能修饰前一个元素,真正参与当前字符比较的是 p[j - 2]

2. 忘记"匹配零次"

只实现重复匹配会导致 a*b 无法匹配 b。必须包含 dp[i][j - 2]

3. 使用 dp[i - 1][j - 2] 表示重复

重复一次或多次时,模式仍然停留在 *,正确状态是 dp[i - 1][j]

4. 忘记初始化空字符串

a*a*b* 可以匹配空字符串,第一行必须按星号结构初始化。

5. 实现成部分匹配

答案必须取 dp[m][n],表示字符串和模式都被完整消费。

十三、总结

这道题的核心只有两类转移:

  1. 普通字符或点号匹配时,继承左上角状态;
  2. 星号匹配零次时跳过两个模式字符;
  3. 星号匹配一次或多次时,消费一个字符串字符并保留模式位置。

只要理解 dp[i][j - 2]dp[i - 1][j] 分别代表什么,整个动态规划就会非常清晰。

更多技术实践与案例,可在 📗【姚前述】中查阅。

相关推荐
落魄大学生之流水线上谋生计3 小时前
正则表达式完全指南:从入门到精通
正则表达式
LayZhangStrive2 天前
设计模式(二)装饰器模式、代理模式(待更新)
设计模式·代理模式·装饰器模式·后端开发
tianyu2342 天前
Java 正则表达式终极指南:从 Pattern 到 Matcher,从双重转义到性能优化,一篇全搞定
java·开发语言·正则表达式·group·find·pattern·matcher
OPEN-F4 天前
Python进阶教程:正则表达式进阶与文本处理
开发语言·python·正则表达式
Lam Tang7 天前
APS 系列文章10
java·代理模式
小小龙学IT7 天前
C++ 正则表达式完全指南:从 std::regex 实战到 RE2 引擎原理(NFA/DFA/回溯陷阱)
c++·正则表达式
傻啦嘿哟7 天前
猫眼电影TOP100爬虫:Requests+正则表达式实战
爬虫·正则表达式
circuitsosk7 天前
AI输出的“质检员”:构建智能体质量评估、异常检测与人工兜底的三层防线
人工智能·python·microsoft·正则表达式·langchain
Lam Tang7 天前
APS 系列文章 08
java·代理模式