正则表达式匹配 - 动态规划
正则表达式匹配 - 动态规划详解
目录
- 问题理解
- 测试用例分析
- 思路分析
- 递归解法
- 记忆化递归
- 动态规划解法
- 图解过程
- 完整代码实现
- 复杂度分析
- 常见错误
问题理解
问题描述
给定一个字符串 s
和一个字符规律 p
,实现支持 '.'
和 '*'
的正则表达式匹配。
规则:
'.'
匹配任意单个字符'*'
匹配零个或多个前面的那一个元素- 匹配应该覆盖整个字符串
s
关键理解点
1. '.'
的含义
'.'
可以匹配任何单个字符。
例子:
s = "ab"
p = "a." → 匹配成功('.' 匹配 'b')
p = ".b" → 匹配成功('.' 匹配 'a')
p = ".." → 匹配成功(两个 '.' 分别匹配 'a' 和 'b')
2. '*'
的含义
'*'
不是单独存在的,它总是跟在某个字符后面,表示前面的字符可以出现0次或多次。
例子:
s = "aa"
p = "a*" → 匹配成功('a' 出现2次)s = "ab"
p = "a*b" → 匹配成功('a' 出现1次)s = "b"
p = "a*b" → 匹配成功('a' 出现0次)s = ""
p = "a*" → 匹配成功('a' 出现0次)
3. ".*"
的含义
这是最强大的组合:可以匹配任意字符的任意次数(包括空字符串)。
例子:
s = "anything"
p = ".*" → 匹配成功(匹配整个字符串)s = "abc"
p = "a.*c" → 匹配成功(中间可以是任意字符)s = "aab"
p = "c*a*b" → 匹配成功('c' 出现0次,'a' 出现2次)
测试用例分析
基础用例
// 用例1:完全相同
s = "abc", p = "abc" → true// 用例2:使用 '.'
s = "abc", p = "a.c" → true
s = "abc", p = "..." → true// 用例3:使用 '*' 匹配0次
s = "ab", p = "a*ab" → true('a' 出现0次)// 用例4:使用 '*' 匹配多次
s = "aaa", p = "a*" → true('a' 出现3次)
s = "aaa", p = "ab*a*" → true('b' 出现0次,第一个'a'出现1次,第二个'a'出现2次)
边界用例
// 空字符串
s = "", p = "" → true
s = "", p = "a*" → true('a' 出现0次)
s = "", p = ".*" → true(任意字符出现0次)
s = "a", p = "" → false// 复杂组合
s = "mississippi", p = "mis*is*p*." → false
s = "aab", p = "c*a*b" → true
错误理解的用例
// 注意:'*' 不能单独匹配
s = "ab", p = "*" → 无效的模式(题目保证不会出现)// 注意:必须匹配整个字符串
s = "aa", p = "a" → false(只匹配了一个'a',不是整个字符串)
思路分析
核心思想
这是一个典型的字符串匹配问题,需要考虑多种情况。关键是如何处理 '*'
。
分析过程
从字符串的末尾开始思考(也可以从开头,但末尾更直观):
情况1:没有 ‘*’
如果 p
的当前字符不是 '*'
,那么:
- 如果
s
和p
当前字符匹配(相等或p
是'.'
),继续匹配剩余部分 - 否则,匹配失败
s = "abc"
p = "abc"比较 'c' 和 'c':匹配 ✓
比较 'b' 和 'b':匹配 ✓
比较 'a' 和 'a':匹配 ✓
结果:true
情况2:遇到 ‘*’
如果 p
的下一个字符是 '*'
,有两种选择:
选择A:'*'
匹配0次
- 跳过
p
当前字符和'*'
,继续匹配
选择B:'*'
匹配1次或多次
- 如果
s
当前字符和p
当前字符匹配,s
向前移动一位,p
保持不变(因为'*'
可以继续匹配)
例子:s = "aaa", p = "a*"尝试1:'a*' 匹配3个 'a'比较 'a' 和 'a':匹配,s向前,p保持比较 'a' 和 'a':匹配,s向前,p保持比较 'a' 和 'a':匹配,s向前,p保持s 和 p 都到达末尾 → 成功 ✓
递归结构
定义函数:isMatch(s, i, p, j)
表示从 s[i:]
和 p[j:]
开始是否匹配。
isMatch(s, i, p, j):// 基础情况if j == p.length:return i == s.length// 判断当前字符是否匹配firstMatch = (i < s.length) && (p[j] == s[i] || p[j] == '.')// 如果下一个字符是 '*'if j + 1 < p.length && p[j + 1] == '*':// 选择A:'*' 匹配0次// 选择B:'*' 匹配至少1次(需要当前字符匹配)return isMatch(s, i, p, j + 2) || // '*' 匹配0次(firstMatch && isMatch(s, i + 1, p, j)) // '*' 匹配至少1次else:// 没有 '*',直接匹配return firstMatch && isMatch(s, i + 1, p, j + 1)
递归解法
代码实现
public class RegexMatching {/*** 递归解法(暴力搜索)* 时间复杂度:O(2^(m+n)) - 指数级,非常慢*/public static boolean isMatchRecursive(String s, String p) {return recursiveHelper(s, 0, p, 0);}private static boolean recursiveHelper(String s, int i, String p, int j) {// 基础情况:模式串用完了if (j == p.length()) {return i == s.length(); // 看文本串是否也用完了}// 判断当前位置是否匹配boolean firstMatch = (i < s.length()) && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');// 如果下一个字符是 '*'if (j + 1 < p.length() && p.charAt(j + 1) == '*') {// 两种选择:// 1. '*' 匹配0次:跳过当前字符和'*'// 2. '*' 匹配至少1次:当前字符匹配,文本串前进,模式串不动return recursiveHelper(s, i, p, j + 2) || // 匹配0次(firstMatch && recursiveHelper(s, i + 1, p, j)); // 匹配>=1次} else {// 没有 '*',必须当前字符匹配,且后续也匹配return firstMatch && recursiveHelper(s, i + 1, p, j + 1);}}
}
递归树示例
以 s = "aa", p = "a*"
为例:
isMatch("aa", "a*")
├─ '*' 匹配0次: isMatch("aa", "") → false
└─ '*' 匹配>=1次: 'a'匹配'a' ✓└─ isMatch("a", "a*")├─ '*' 匹配0次: isMatch("a", "") → false└─ '*' 匹配>=1次: 'a'匹配'a' ✓└─ isMatch("", "a*")├─ '*' 匹配0次: isMatch("", "") → true ✓└─ (s为空,不能继续匹配)结果:true
递归解法的问题
重复计算:相同的 (i, j)
会被多次计算。
例如 s = "aaa", p = "a*a*a*"
会产生大量重复子问题。
记忆化递归
优化思路
用一个二维数组(或HashMap)缓存已经计算过的结果。
代码实现
public class RegexMatchingMemo {private Boolean[][] memo; // 使用Boolean而不是boolean,因为需要null表示未计算/*** 记忆化递归* 时间复杂度:O(m * n)* 空间复杂度:O(m * n)*/public boolean isMatch(String s, String p) {memo = new Boolean[s.length() + 1][p.length() + 1];return memoHelper(s, 0, p, 0);}private boolean memoHelper(String s, int i, String p, int j) {// 检查缓存if (memo[i][j] != null) {return memo[i][j];}boolean result;// 基础情况if (j == p.length()) {result = (i == s.length());} else {// 判断当前字符是否匹配boolean firstMatch = (i < s.length()) && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');// 处理 '*'if (j + 1 < p.length() && p.charAt(j + 1) == '*') {result = memoHelper(s, i, p, j + 2) || // '*' 匹配0次(firstMatch && memoHelper(s, i + 1, p, j)); // '*' 匹配>=1次} else {result = firstMatch && memoHelper(s, i + 1, p, j + 1);}}// 缓存结果memo[i][j] = result;return result;}// 带调试输出的版本public boolean isMatchWithDebug(String s, String p) {memo = new Boolean[s.length() + 1][p.length() + 1];boolean result = memoHelperDebug(s, 0, p, 0, 0);// 打印缓存表System.out.println("\n缓存表(memo):");System.out.print(" ");for (int j = 0; j < p.length(); j++) {System.out.print(p.charAt(j) + " ");}System.out.println("$");for (int i = 0; i <= s.length(); i++) {if (i < s.length()) {System.out.print(s.charAt(i) + " ");} else {System.out.print("$ ");}for (int j = 0; j <= p.length(); j++) {if (memo[i][j] == null) {System.out.print("- ");} else {System.out.print((memo[i][j] ? "T" : "F") + " ");}}System.out.println();}return result;}private boolean memoHelperDebug(String s, int i, String p, int j, int depth) {// 打印当前状态String indent = " ".repeat(depth);System.out.println(indent + "匹配: s[" + i + ":] = \"" + s.substring(i) + "\" vs p[" + j + ":] = \"" + p.substring(j) + "\"");if (memo[i][j] != null) {System.out.println(indent + "→ 从缓存返回: " + memo[i][j]);return memo[i][j];}boolean result;if (j == p.length()) {result = (i == s.length());System.out.println(indent + "→ 基础情况: " + result);} else {boolean firstMatch = (i < s.length()) && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');if (j + 1 < p.length() && p.charAt(j + 1) == '*') {System.out.println(indent + "→ 遇到 '*',尝试两种选择");result = memoHelperDebug(s, i, p, j + 2, depth + 1) ||(firstMatch && memoHelperDebug(s, i + 1, p, j, depth + 1));} else {System.out.println(indent + "→ 普通字符匹配");result = firstMatch && memoHelperDebug(s, i + 1, p, j + 1, depth + 1);}}memo[i][j] = result;System.out.println(indent + "→ 结果: " + result);return result;}
}
动态规划解法
状态定义
dp[i][j]
表示:s
的前 i
个字符和 p
的前 j
个字符是否匹配。
注意:
dp[0][0] = true
(两个空字符串匹配)dp[i][0] = false
(s
非空,p
为空,不匹配)dp[0][j]
需要特殊处理(s
为空,p
可能通过*
匹配空串)
状态转移方程
情况1:p[j-1]
不是 '*'
if p[j-1] == s[i-1] || p[j-1] == '.':dp[i][j] = dp[i-1][j-1]
else:dp[i][j] = false
情况2:p[j-1]
是 '*'
'*'
要和前面的字符 p[j-2]
一起考虑:
选择A:'*'
匹配0次(忽略 p[j-2]
和 p[j-1]
)
dp[i][j] = dp[i][j-2]
选择B:'*'
匹配至少1次(需要 s[i-1]
和 p[j-2]
匹配)
if p[j-2] == s[i-1] || p[j-2] == '.':dp[i][j] = dp[i-1][j]
综合:
if p[j-1] == '*':dp[i][j] = dp[i][j-2] // 匹配0次if p[j-2] == s[i-1] || p[j-2] == '.':dp[i][j] = dp[i][j] || dp[i-1][j] // 匹配>=1次
完整状态转移
if (p.charAt(j-1) == '*') {// 情况2:遇到 '*'dp[i][j] = dp[i][j-2]; // '*' 匹配0次if (p.charAt(j-2) == s.charAt(i-1) || p.charAt(j-2) == '.') {dp[i][j] = dp[i][j] || dp[i-1][j]; // '*' 匹配>=1次}
} else {// 情况1:普通字符或 '.'if (p.charAt(j-1) == s.charAt(i-1) || p.charAt(j-1) == '.') {dp[i][j] = dp[i-1][j-1];}
}
初始化
// 1. 空字符串匹配
dp[0][0] = true;// 2. s为空,p不为空
for (int j = 2; j <= p.length(); j++) {if (p.charAt(j-1) == '*') {dp[0][j] = dp[0][j-2]; // 通过 '*' 匹配0次来消除字符}
}// 3. s不为空,p为空
// dp[i][0] = false (默认值)
代码实现
public class RegexMatchingDP {/*** 动态规划解法(自底向上)* 时间复杂度:O(m * n)* 空间复杂度:O(m * n)*/public static boolean isMatch(String s, String p) {int m = s.length();int n = p.length();// dp[i][j] 表示 s 的前 i 个字符和 p 的前 j 个字符是否匹配boolean[][] dp = new boolean[m + 1][n + 1];// 初始化dp[0][0] = true; // 空字符串匹配// 初始化第一行:s为空,p不为空// 只有 "a*", "a*b*", ".*" 这种可以匹配空串for (int j = 2; j <= n; j++) {if (p.charAt(j - 1) == '*') {dp[0][j] = dp[0][j - 2];}}// 填充dp表for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {char sc = s.charAt(i - 1); // s的当前字符char pc = p.charAt(j - 1); // p的当前字符if (pc == '*') {// 情况2:遇到 '*'char prevPc = p.charAt(j - 2); // '*' 前面的字符// 选择A:'*' 匹配0次dp[i][j] = dp[i][j - 2];// 选择B:'*' 匹配至少1次(需要当前字符匹配)if (prevPc == sc || prevPc == '.') {dp[i][j] = dp[i][j] || dp[i - 1][j];}} else {// 情况1:普通字符或 '.'if (pc == sc || pc == '.') {dp[i][j] = dp[i - 1][j - 1];}// else: dp[i][j] = false (默认值)}}}return dp[m][n];}/*** 带详细注释和调试输出的版本*/public static boolean isMatchWithDebug(String s, String p) {int m = s.length();int n = p.length();boolean[][] dp = new boolean[m + 1][n + 1];System.out.println("匹配: s = \"" + s + "\", p = \"" + p + "\"");System.out.println();// 初始化dp[0][0] = true;System.out.println("初始化: dp[0][0] = true (空字符串匹配)");// 初始化第一行for (int j = 2; j <= n; j++) {if (p.charAt(j - 1) == '*') {dp[0][j] = dp[0][j - 2];System.out.println("初始化: dp[0][" + j + "] = " + dp[0][j] + " (p的前" + j + "个字符\"" + p.substring(0, j) + "\"" + (dp[0][j] ? "可以" : "不能") + "匹配空串)");}}System.out.println();// 填充dp表for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {char sc = s.charAt(i - 1);char pc = p.charAt(j - 1);System.out.println("处理: s[" + (i-1) + "] = '" + sc + "', p[" + (j-1) + "] = '" + pc + "'");if (pc == '*') {char prevPc = p.charAt(j - 2);System.out.println(" 遇到'*',前一个字符是'" + prevPc + "'");// 匹配0次dp[i][j] = dp[i][j - 2];System.out.println(" 选择A(匹配0次): dp[" + i + "][" + j + "] = dp[" + i + "][" + (j-2) + "] = " + dp[i][j]);// 匹配至少1次if (prevPc == sc || prevPc == '.') {boolean option = dp[i - 1][j];System.out.println(" 选择B(匹配>=1次): dp[" + i + "][" + j + "] = dp[" + (i-1) + "][" + j + "] = " + option);dp[i][j] = dp[i][j] || option;}} else {if (pc == sc || pc == '.') {dp[i][j] = dp[i - 1][j - 1];System.out.println(" 字符匹配: dp[" + i + "][" + j + "] = dp[" + (i-1) + "][" + (j-1) + "] = " + dp[i][j]);} else {System.out.println(" 字符不匹配: dp[" + i + "][" + j + "] = false");}}System.out.println(" 结果: dp[" + i + "][" + j + "] = " + dp[i][j]);System.out.println();}}// 打印dp表printDpTable(s, p, dp);return dp[m][n];}/*** 打印DP表格*/private static void printDpTable(String s, String p, boolean[][] dp) {System.out.println("DP表格:");// 打印表头System.out.print(" ");for (int j = 0; j < p.length(); j++) {System.out.printf("%3c", p.charAt(j));}System.out.println();// 打印分隔线System.out.print(" ");for (int j = 0; j <= p.length(); j++) {System.out.print("---");}System.out.println();// 打印每一行for (int i = 0; i <= s.length(); i++) {if (i == 0) {System.out.print(" ε ");} else {System.out.printf("%3c ", s.charAt(i - 1));}for (int j = 0; j <= p.length(); j++) {System.out.printf("%3s", dp[i][j] ? "T" : "F");}System.out.println();}System.out.println();}
}
图解过程
示例1:s = “aa”, p = “a*”
DP表格构建过程
初始状态:a *---------ε | T F Ta | F ? ?a | F ? ?解释:
- dp[0][0] = true (空匹配空)
- dp[0][1] = false (空不能匹配"a")
- dp[0][2] = true (空可以匹配"a*",'*'匹配0次)
填充 dp[1][1]:s[0]='a', p[0]='a'
字符相同 → dp[1][1] = dp[0][0] = truea *---------ε | T F Ta | F T ?a | F ? ?
填充 dp[1][2]:s[0]='a', p[1]='*'
'*' 前是 'a',与 s[0]='a' 匹配
选择A(匹配0次): dp[1][0] = false
选择B(匹配1次): dp[0][2] = true
dp[1][2] = false || true = truea *---------ε | T F Ta | F T Ta | F ? ?
填充 dp[2][1]:s[1]='a', p[0]='a'
字符相同,但 dp[1][0] = false
dp[2][1] = falsea *---------ε | T F Ta | F T Ta | F F ?
填充 dp[2][2]:s[1]='a', p[1]='*'
'*' 前是 'a',与 s[1]='a' 匹配
选择A(匹配0次): dp[2][0] = false
选择B(匹配2次): dp[1][2] = true
dp[2][2] = false || true = truea *---------ε | T F Ta | F T Ta | F F T ← 答案
结果:dp[2][2] = true ✓
示例2:s = “ab”, p = “.*”
初始状态:. *---------ε | T F Ta | F ? ?b | F ? ?
填充过程:dp[1][1]: s[0]='a', p[0]='.'
'.' 匹配任意字符 → dp[1][1] = dp[0][0] = truedp[1][2]: s[0]='a', p[1]='*'
'*' 前是 '.',匹配任意字符
选择B(匹配>=1次): dp[0][2] = true
dp[1][2] = truedp[2][1]: s[1]='b', p[0]='.'
'.' 匹配任意字符 → dp[2][1] = dp[1][0] = falsedp[2][2]: s[1]='b', p[1]='*'
'*' 前是 '.',匹配任意字符
选择B(匹配>=1次): dp[1][2] = true
dp[2][2] = true最终表格:. *---------ε | T F Ta | F T Tb | F F T ← 答案
结果:dp[2][2] = true ✓
示例3:s = “aab”, p = “cab”
初始状态:c * a * b------------------ε | T F T F T Fa | F ? ? ? ? ?a | F ? ? ? ? ?b | F ? ? ? ? ?解释dp[0]行:
- dp[0][0] = true
- dp[0][2] = true (c* 可以匹配空)
- dp[0][4] = true (c*a* 可以匹配空)
逐步填充:第1行 (s[0]='a'):
- dp[1][1]: 'a' vs 'c' → false
- dp[1][2]: 'a' vs 'c*' → c*匹配0次 → dp[1][0]=false
- dp[1][3]: 'a' vs 'a' → dp[1][3]=dp[0][2]=true
- dp[1][4]: 'a' vs 'a*' → a*匹配1次 → dp[0][4]=true → dp[1][4]=true
- dp[1][5]: 'a' vs 'b' → false第2行 (s[1]='a'):
- dp[2][3]: 'a' vs 'a' → dp[2][3]=dp[1][2]=false
- dp[2][4]: 'a' vs 'a*' → a*匹配2次 → dp[1][4]=true → dp[2][4]=true
- dp[2][5]: 'a' vs 'b' → false第3行 (s[2]='b'):
- dp[3][5]: 'b' vs 'b' → dp[3][5]=dp[2][4]=true ✓最终表格:c * a * b------------------ε | T F T F T Fa | F F F T T Fa | F F F F T Fb | F F F F F T ← 答案
结果:dp[3][5] = true ✓
完整代码实现
主类(包含所有方法)
public class Solution {// ==================== 方法1:递归(暴力) ====================public boolean isMatchRecursive(String s, String p) {return recursiveHelper(s, 0, p, 0);}private boolean recursiveHelper(String s, int i, String p, int j) {if (j == p.length()) {return i == s.length();}boolean firstMatch = (i < s.length()) && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');if (j + 1 < p.length() && p.charAt(j + 1) == '*') {return recursiveHelper(s, i, p, j + 2) ||(firstMatch && recursiveHelper(s, i + 1, p, j));} else {return firstMatch && recursiveHelper(s, i + 1, p, j + 1);}}// ==================== 方法2:记忆化递归 ====================private Boolean[][] memo;public boolean isMatchMemo(String s, String p) {memo = new Boolean[s.length() + 1][p.length() + 1];return memoHelper(s, 0, p, 0);}private boolean memoHelper(String s, int i, String p, int j) {if (memo[i][j] != null) {return memo[i][j];}boolean result;if (j == p.length()) {result = (i == s.length());} else {boolean firstMatch = (i < s.length()) && (p.charAt(j) == s.charAt(i) || p.charAt(j) == '.');if (j + 1 < p.length() && p.charAt(j + 1) == '*') {result = memoHelper(s, i, p, j + 2) ||(firstMatch && memoHelper(s, i + 1, p, j));} else {result = firstMatch && memoHelper(s, i + 1, p, j + 1);}}memo[i][j] = result;return result;}// ==================== 方法3:动态规划(推荐) ====================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];}}// 填充dp表for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {char sc = s.charAt(i - 1);char pc = p.charAt(j - 1);if (pc == '*') {char prevPc = p.charAt(j - 2);dp[i][j] = dp[i][j - 2]; // 匹配0次if (prevPc == sc || prevPc == '.') {dp[i][j] = dp[i][j] || dp[i - 1][j]; // 匹配>=1次}} else {if (pc == sc || pc == '.') {dp[i][j] = dp[i - 1][j - 1];}}}}return dp[m][n];}
}
测试类
public class Test {public static void main(String[] args) {Solution solution = new Solution();// 测试用例String[][] testCases = {{"aa", "a", "false"},{"aa", "a*", "true"},{"ab", ".*", "true"},{"aab", "c*a*b", "true"},{"mississippi", "mis*is*p*.", "false"},{"", "a*", "true"},{"", ".*", "true"},{"a", "", "false"},{"", "", "true"}};System.out.println("=== 正则表达式匹配测试 ===\n");for (String[] test : testCases) {String s = test[0];String p = test[1];boolean expected = Boolean.parseBoolean(test[2]);boolean result1 = solution.isMatchRecursive(s, p);boolean result2 = solution.isMatchMemo(s, p);boolean result3 = solution.isMatch(s, p);System.out.println("输入: s = \"" + s + "\", p = \"" + p + "\"");System.out.println("期望: " + expected);System.out.println("递归: " + result1 + (result1 == expected ? " ✓" : " ✗"));System.out.println("记忆化: " + result2 + (result2 == expected ? " ✓" : " ✗"));System.out.println("动态规划: " + result3 + (result3 == expected ? " ✓" : " ✗"));System.out.println();}}
}
复杂度分析
时间复杂度
方法 | 时间复杂度 | 说明 |
---|---|---|
递归 | O(2^(m+n)) | 指数级,存在大量重复计算 |
记忆化递归 | O(m × n) | 每个状态只计算一次 |
动态规划 | O(m × n) | 填充 (m+1)×(n+1) 的表 |
其中 m 是字符串 s 的长度,n 是模式 p 的长度。
空间复杂度
方法 | 空间复杂度 | 说明 |
---|---|---|
递归 | O(m + n) | 递归调用栈的深度 |
记忆化递归 | O(m × n) | memo数组 + 递归栈 |
动态规划 | O(m × n) | dp数组 |
空间优化
动态规划可以优化到 O(n),因为每次只需要前一行的数据:
public boolean isMatchOptimized(String s, String p) {int m = s.length();int n = p.length();// 只需要两行boolean[] prev = new boolean[n + 1];boolean[] curr = new boolean[n + 1];// 初始化prev[0] = true;for (int j = 2; j <= n; j++) {if (p.charAt(j - 1) == '*') {prev[j] = prev[j - 2];}}// 填充for (int i = 1; i <= m; i++) {curr[0] = false; // s非空,p为空for (int j = 1; j <= n; j++) {char sc = s.charAt(i - 1);char pc = p.charAt(j - 1);if (pc == '*') {char prevPc = p.charAt(j - 2);curr[j] = curr[j - 2];if (prevPc == sc || prevPc == '.') {curr[j] = curr[j] || prev[j];}} else {if (pc == sc || pc == '.') {curr[j] = prev[j - 1];} else {curr[j] = false;}}}// 交换boolean[] temp = prev;prev = curr;curr = temp;}return prev[n];
}
常见错误
错误1:索引越界
// 错误写法
if (p.charAt(j) == '*') { // 当 j=0 时会越界// ...
}// 正确写法
if (j > 0 && p.charAt(j-1) == '*') {// ...
}
错误2:‘*’ 单独处理
// 错误:把 '*' 当作独立字符
if (p.charAt(j) == '*') {// 错误!'*' 必须和前面的字符一起考虑
}// 正确:检查下一个字符是否是 '*'
if (j + 1 < p.length() && p.charAt(j + 1) == '*') {// 正确!考虑当前字符和后面的 '*'
}
错误3:忘记初始化边界
// 错误:没有初始化 dp[0][j]
dp[0][0] = true;
// 缺少对 "a*", "a*b*" 等能匹配空串的处理// 正确:
dp[0][0] = true;
for (int j = 2; j <= n; j++) {if (p.charAt(j - 1) == '*') {dp[0][j] = dp[0][j - 2];}
}
错误4:状态转移逻辑错误
// 错误:只考虑匹配0次
if (pc == '*') {dp[i][j] = dp[i][j - 2];
}// 正确:考虑匹配0次和>=1次
if (pc == '*') {dp[i][j] = dp[i][j - 2]; // 匹配0次if (prevPc == sc || prevPc == '.') {dp[i][j] = dp[i][j] || dp[i - 1][j]; // 匹配>=1次}
}
总结
核心要点
-
理解题意:
'.'
匹配任意单个字符'*'
匹配0个或多个前面的字符- 必须匹配整个字符串
-
状态定义:
dp[i][j]
=s
的前i
个字符和p
的前j
个字符是否匹配
-
状态转移:
- 普通字符:当前字符匹配且前面也匹配
'*'
:可以匹配0次或多次
-
边界处理:
- 空字符串的情况
'*'
可以让前面的字符出现0次,从而匹配空串
解题技巧
- 从简单到复杂:先写递归,再优化为记忆化,最后改为DP
- 画图辅助:用表格展示dp数组的填充过程
- 充分测试:考虑各种边界情况和特殊组合
推荐做法
对于这道题,推荐使用动态规划方法:
- 代码清晰易懂
- 时间复杂度最优
- 便于调试和验证