Leetcode44 通配符匹配
LeetCode第44题是leetcode难度为hard的一个,解题方法是使用动态规划。题目内容可点击下面的黑色三角展开题目详情。
点击展开题目详情
题目来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/wildcard-matching
给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。 ’?’ 可以匹配任何单个字符。 ’*’ 可以匹配任意字符串(包括空字符串)。 两个字符串完全匹配才算匹配成功。 说明:
s 可能为空,且只包含从 a-z 的小写字母。 p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。 示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = “*”
输出: true
解释: ‘*’ 可以匹配任意字符串。
示例 3:
输入:
s = “cb”
p = “?a”
输出: false
解释: ‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’。
示例 4:
输入:
s = “adceb”
p = “*a*b”
输出: true
解释: 第一个 ‘*’ 可以匹配空字符串, 第二个 ‘*’ 可以匹配字符串 “dce”.
这道题的难点在于对星号(*)的处理。
点击展开字符模式中不包含\*的情况
**个人认为如果没有\*就是另外一种思路,建议直接看包含\*的情况,但是也可以把没有\*的情况当练练手。** # 字符模式中不包含\*的情况Case 1
输入:
s: 为空
p: 为空
输出:
true
演绎
null | |
---|---|
null | 1 |
Case 2
输入:
s: azc
p: ab?
输出:
false
演绎
null | a | z | c | |
---|---|---|---|---|
null | 1 | |||
a | 1 | |||
b | 0 | |||
? |
Case 3
输入:
s: abc
p: ?b??
输出:
false
演绎
null | ? | b | ? | ? | |
---|---|---|---|---|---|
null | 1 | ||||
a | 1 | ||||
b | 1 | ||||
c | 1 |
说明
如Case 1所示,base case当s和p均为空的情况下,匹配成功,记为1.
如Case 2,3所示,在没有*的情况下,s和q上的每一格应该是一一对应的关系,所以只需要沿着对角线一一匹配就可以了。
满足公式:s[i]==p[j] 或 s[i] == '?'
时,表示前i个p和s成功匹配,
如Case 2所示,当前i个中出现不能匹配的情况,则后面就不需要匹配了,即当前i个字符出现不匹配的情况,前i+1个字符也必然不匹配,如若az不能与ab匹配,则azc也必然不能与ab?匹配。
所以当扫描到第i行第i列出现不匹配时,可以直接输出结果false。
如Case 3所示,当匹配到对角线的最后一格(Case 3图中加粗字体处的[c,?]),若仍有需要匹配的(即右方或下方还有空格),表示匹配失败。若没有需要匹配的,表示匹配成功,返回true。
字符模式中包含*的情况
输入:
s: abab
p: *b*
输出:
true
演绎
null | * | b | * | |
---|---|---|---|---|
null | 1 | ①1 | ||
a | ①1 | ②0 | ||
b | ①1 | ③1 | ⑥1 | |
a | ①1 | ④0 | ⑥1 | |
b | ①1 | ⑤1 | ⑥1 |
过程
Step 1. 初始化[null,null]为1.
Step 2. 扫描到p[0]处的*, 找到左格为1的格子,从这一格开始(包括这一格)往下一列格子置1(即将表格中的①全部置1).
Step 3. 扫描到p[1]处的b,找到左上格(即[null,*])为1的格子, 从这个格子开始从上到下依次进行字符匹配运算,字符匹配运算公式为左上格==1 and (s[i]==p[j] or s[i] == '?')
.
Step 4. 扫描到p[2]处的*, 找到左格(即④)为1的格子, 从这个格子开始(包括这一格)往下的所有列置为1(即将表格中的⑥全部置1).
解释
思考问题1. 为什么扫描到*需要找左格为1的格子?之后又需要往下一列置1?
因为*能匹配空串,所以要找左格为1.
如下表,“a*“可以和"a"匹配:
null | a | * | |
---|---|---|---|
null | 1 | ||
a | 1 | 1 | |
因为*能够匹配字串后面所有的字符,所以下面可以一次性全部置1. | |||
如下表,“a*“可以和"ab"匹配,也可以和"abb匹配”: |
null | a | * | |
---|---|---|---|
null | 1 | ||
a | 1 | 1 | |
b | 1 | 1 | |
b | 1 | 1 |
思考问题2. 为什么扫描到非*找到左上格为1的格子?之后又需要从上到下依次进行匹配计算?
先看为什么要依次比较一遍,这是*通配符带来的,看下面的例子。"*b"与"a"不匹配不意味着”*b"与"ab"不匹配。
null | * | b | ? | |
---|---|---|---|---|
null | 1 | 1 | ||
a | 1 | 0 | ||
b | 1 | 1 | ||
b | 1 | 1 | 1 | |
在扫描非*的时候,我们希望找到左上方为1的格子(通过对比上下两个表格感受一下)。 |
在下面的表格(把上面的表格中的*换成?)中:
我们注意到"ab"与”?“不匹配,“abb"与”?b"也必然不匹配(即使后面的匹配上了p[1]==s[2]==b,前面的子串没匹配上也白搭)
null | ? | b | ? | |
---|---|---|---|---|
null | 1 | |||
a | 1 | 0 | ||
b | 0 | 1 | ||
b | 0 | 0 | 1 |
点击展开代码
我写的python3实现代码,感觉简洁度上差了一点,主要是为了便于理解,多了一些中间变量什么的。 而且实际上table不需要显式写在代码里的,因为可以发现table只需要记录扫描到的部分的左边一列就够了。但个人认为作为学习来讲,掌握算法思路的前提下,保障代码可懂性也是必要的。 ```python3 class Solution:def isMatch(self, s: str, p: str) -> bool:
table = []
n_row = len(s) + 1
n_column = len(p) + 1
table = [[None] * n_column for _ in range(n_row)]
table[0][0] = True
for j in range(len(p)):
table_j = j+1
cur_p = p[j] #current p当前扫描到的通配符
left_column = [e[table_j-1] for e in table]
if cur_p == '*':
# 找到左格为1的格子, 注意一下p[0]所在位置对应的是table[:][1]
has_one = False
for table_i,left in enumerate(left_column):
if left == True:
# 从这一格开始往下全部置为1
for sub_i in range(table_i,n_row):
has_one = True
table[sub_i][table_j] = True
break
if has_one == False: #左格没有为1的格子
return False
else:
# 找到左上格为1的格子.如果没有则返回false
has_one = False
for start_i,left in enumerate(left_column):
if left == True and start_i<n_row-1: #如果只有最后一行是1也不行
has_one = True
break
if has_one == False:
return False
for table_i in range(start_i+1,n_row):
if table[table_i-1][table_j-1] == 1 and (cur_p == '?' or cur_p == s[table_i-1]):
table[table_i][table_j] = True
else:
table[table_i][table_j] = False
if table[-1][-1] == None: #p扫描完了,但是s还有多余。比如"ab"和"?"
return False
return table[-1][-1]
|
|
下图为展开代码中的另一种方法的示意图:
null | * | b | c | * | ? | |
---|---|---|---|---|---|---|
null | 1 | |||||
a | ①1 | ②0 | ||||
b | ③1 | ④1 | ||||
b | ⑥1 | ⑦1 | ⑤0 | |||
c | ⑧1 | |||||
d | ⑨1 |
推荐阅读