44. 通配符匹配

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。

两个字符串完全匹配才算匹配成功。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

示例:

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

输入:
s = "aa"
p = "*"
输出: true
解释: '*' 可以匹配任意字符串。

分析:

10. 正则表达式匹配很像,这里贴出第10题与本题的区别:

'*' 匹配零个或多个前面的那一个元素

即第10题需要给出a*才能匹配aaaa,而本题只需要给出a

这样明显降低了难度,我们不再需要考虑*之前的字符了。

虽然之前做过第10题,还是想自己顺一遍动态规划的过程:

  1. 写出dp结构
  2. 抓住转移方程;
  3. 考虑边界条件;

对于本题,首先考虑如何规划dp数组。假设dp[i],可以表示为s字符串的前i个字符可以被p字符串匹配,或者p字符串的前i个字符可以匹配s字符串。由于本题要求 两个字符串都要完全匹配,显然不满足要求。所以需要二重dp数组 dp[i][j],表示 s字符串前i个字符可以被p字符串前j个字符匹配

接下来考虑转移方程,①:p[j] != '*'时,只需要考虑s[i] == p[j] or p[j] == '?'表示单个字符被匹配。

即有:dp[i][j] == (s[i] == p[j] or p[j] == '?') and dp[i - 1][j - 1] # p[j] != "*"

②:如果考虑 p[j] == '*',这时有两种情况

  1. 例如:s = "abcdef", p = "abc*"
    这时*起到了匹配多个字符的作用,此时只需要考察p字符串*前的字符是否都被匹配,不断向下即可。
    dp[i][j] = dp[i - 1][j]
  2. 例如:s = "abcdef", p = "abc*def"
    *相当于空字符,直接跳过不管。
    dp[i][j] = dp[i][j - 1]

总结:

\(dp[i][j] = \begin{cases}(s[i]==p[j])\ and\ dp[i-1][j-1] &\text{if p[j] != '*'}\\ dp[i-1][j]\ or\ dp[i][j-1] &\text{if p[j] == '*'}\end{cases}\)

最后考虑边界条件:

  1. 如果s,p都为空,符合要求,即dp[0][0] == true
  2. 由于*可以匹配空字符,因此dp[0][j] == true if p[j] == '\*' and dp[0][j - 1]

代码(Golang):

func isMatch(s string, p string) bool {
	m, n := len(s), len(p)
	dp := make([][]bool, m + 1)
	for i := 0; i <= m; i++ {
		dp[i] = make([]bool, n + 1)
	}
	dp[0][0] = true
	for i := 0; i <= n; i++ {
		if p[i - 1] == '*' {
			dp[0][i] = true
		} else {
			break
		}
	}
	for i := 1; i <= m; i++ {
		for j := 1;j <= n; j++ {
			if p[i - 1] == '*' {
				dp[i][j] = dp[i][j - 1] || dp[i - 1][j]
			} else if p[j - 1] == '?' || s[i - 1] == p[j - 1] {
				dp[i][j] = dp[i - 1][j - 1]
			}
		}
	}
	return dp[m][n]
}