题目描述

✅ 剑指 Offer 19. 正则表达式匹配

image-20261001230752543

image-20260928203613737

image-20260928203613738

题意分析

判断模式 p 能否匹配字符串 s 的全部字符。. 匹配任意一个字符,* 让它前面的字符或 . 重复零次或多次。匹配成功时,字符串和模式都必须用完,不能只找到一段局部匹配。

解法:二维动态规划

核心思路

[!blue]

星号可能重复不同次数,直接选择一种次数可能错过答案。用 dp[i][j] 保存 s 的前 $i$ 个字符与 p 的前 $j$ 个字符是否完整匹配,就能合并这些选择并复用较短前缀的结果。这里的 $i$、$j$ 是长度,最后一个字符的下标分别为 i - 1、j - 1。

如果 p[j - 1] 不是星号,它必须消费字符串的一个字符。只有 $i>0$,且两字符相等或模式字符为 . 时,才有 dp[i][j] = dp[i - 1][j - 1];字符对不上时,这个状态就是假。

如果末尾是星号,就把 p[j - 2] 和 * 当成一组。使用零次时,直接删去这一组,继承 dp[i][j - 2]。使用至少一次时,先要求 s[i - 1] 能匹配 p[j - 2],再用这一组消费当前字符,继承 dp[i - 1][j]。模式长度保持不变,才能继续消费更多字符,也能在后续状态选择零次结束重复;两种情况只要有一种成立即可。

初始只有 dp[0][0] = true:空模式不能匹配非空串,因此 dp[i][0] 在 $i>0$ 时为假。空串这一行也要参与计算,普通字符无法匹配它,星号组则可以通过零次分支不断向左退两格,从而判断整个模式是否都能省略。

转移只依赖当前行左侧的 dp[i][j - 2],以及上一行的状态。所以按 $i$ 从 0 到 $m$、$j$ 从 1 到 $n$ 递增计算,依赖都已就绪,最终 dp[m][n] 就表示完整匹配结果。

解题步骤

  • 设字符串、模式长度分别为 $m$、$n$,建立大小为 $(m+1)\times(n+1)$ 的布尔表。
  • 将 dp[0][0] 设为真,其余状态初始为假。
  • 按上述顺序遍历所有前缀;普通字符或 . 检查匹配后继承左上状态。
  • 遇到 * 时,合并零次使用与消费一个字符后继续使用的两条分支。
  • 返回 dp[m][n]。

代码实现

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 i = 0; 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]
                                    || i > 0
                                            && matches(s.charAt(i - 1), p.charAt(j - 2))
                                            && dp[i - 1][j];
                } else if (i > 0 && matches(s.charAt(i - 1), pc)) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
            }
        }

        return dp[m][n];
    }

    private boolean matches(char sc, char pc) {
        return pc == '.' || sc == pc;
    }
}
func isMatch(s string, p string) bool {
    m, n := len(s), len(p)
    dp := make([][]bool, m+1)
    for i := range dp {
        dp[i] = make([]bool, n+1)
    }
    dp[0][0] = true

    matches := func(sc, pc byte) bool {
        return pc == '.' || sc == pc
    }
    // 包含空串这一行,让星号零次使用自然完成初始化
    for i := 0; i <= m; i++ {
        for j := 1; j <= n; j++ {
            pc := p[j-1]
            if pc == '*' {
                // 零次退模式两格,重复使用只退字符串一格并保留星号
                dp[i][j] = dp[i][j-2] ||
                    i > 0 && matches(s[i-1], p[j-2]) && dp[i-1][j]
            } else if i > 0 && matches(s[i-1], pc) {
                dp[i][j] = dp[i-1][j-1]
            }
        }
    }
    return dp[m][n]
}

复杂度分析

  • 时间复杂度:$O((m+1)(n+1))$,包含空串状态。
  • 空间复杂度:$O((m+1)(n+1))$,二维布尔表。

关键点总结

[!green]

  • 星号零次退模式两格,重复只退字符串一格。
  • 读取当前字符前先保证字符串前缀非空。
  • 零次和重复分支可能同时成立,用逻辑或合并即可,不需要提前决定重复次数。

易错点总结

[!yellow]

  • 将重复分支模式也退两格,会限制星号只能使用一次。
  • 漏掉空串行,无法匹配零次组合。
  • 把一次局部匹配当完整成功,未确认两串都被覆盖。
  • * 不能独立匹配任意串;它只能重复前一个元素。题目保证星号前有有效元素,因此星号分支访问 j - 2 是安全的。

相似题目

题目 难度 关联与区别
44. 通配符匹配 困难 两题都是模式匹配,但正则星号重复前一个元素,通配符星号独立匹配任意串,转移不能混用。
115. 不同的子序列 困难 同样按文本与模式的前缀建立二维状态,原题计数匹配子序列,本题判断完整正则匹配。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2021/12988325
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!