正则表达式匹配

正则表达式匹配 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] 分别代表什么,整个动态规划就会非常清晰。

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

相关推荐
承渊政道10 小时前
PR-Agent鸿蒙PC适配全记录:从Python服务端代理到ArkTS原生评审客户端
python·华为·代理模式·agent·harmonyos·pc端
kruptos4 天前
动态规划:暴力递归如何变优雅?
代理模式·算法优化·斐波那契
yume_sibai4 天前
正则表达式完全指南(基础语法 + 实战应用 + 性能优化 + 最佳实践)
开发语言·正则表达式·c#
研☆香5 天前
js中使用的正则表达式
开发语言·javascript·正则表达式
2601_9622935310 天前
浅学python | 正则表达式集锦
正则表达式·字符串匹配·python知识·浅学python·ip地址检查
2601_9622955810 天前
Python中正则表达式的知识汇总分享
python·正则表达式·模块·语法·匹配
启观川10 天前
Python基础-第 16 章 综合案例:客户信息管理系统
开发语言·笔记·python·正则表达式
2601_9622940510 天前
Python 操作 Word:如何查找、替换和批量修改文本
python·正则表达式·word·文档处理·查找替换
for_ever_love__15 天前
python基础语法学习: 正则表达式
python·学习·正则表达式
asdfg125896316 天前
一个例子通俗理解Java中正则表达式用法
java·正则表达式