题目
请实现一个函数来匹配包含’.‘和’*‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串中的所有字符匹配整个模式。
例:
字符串"aaa"与模式"a.a"和“ab*ac*a">匹配,但与"aa.a"和"ab*a"均不匹配。
思路
-
正面暴力深度优先搜索。书上给出的解法。注意使用递归以便处理回溯。(a*a匹配aaa)
约定:待匹配字符串为 s ,匹配模式为 p 。
当p超界时,判断s是否已经全部匹配;
当s超界时,判断p是否已经全部匹配;若不是,剩下的p是否可以匹配空字符串。
当p == s 或者 p == .,过;
当p+1 == *,判断p == s
不匹配,移动向匹配p+2。
匹配, 1-【s继续向下匹配p-1】,2-【此次s匹配下面移动向匹配p+2】,3-【此次不匹配s直接移动向下匹配p+2】(例如a*ab匹配ab,即使a*可以匹配a对于整个字符串此时应当选择跳过)。
程序运行到这里时,将会进行深度优先搜索,试探上述三个情况,当遇见匹配a*a*a*a*a*b匹配aaaaac,时间复杂度将爆炸O(3^n)。然而这里三个情况是有重叠的。下一个思路将会讲解。- 时间复杂度:O(3^n)
- 空间复杂度:O(n) 最深递归
-
从后暴力深度优先搜索。因为*出现在字符后面,如果从前向后遍历,将会需要判断字符后一个是否为 * 。同时分析思路1,对于出现 * 时的策略,2-【此次s匹配下面移动向匹配p+2】,3-【此次不匹配s直接移动向下匹配p+2】,是同一种情况。因为当出现3时,即为选择1->2策略。所以对于 * 根本上只有2种处理,1-【继续匹配】2-【跳过】。
此外,对于边界书上的代码只考虑 s 是否超界时,p是否超界,对于p未超界情况,未分开讨论,导致复杂度增加。- 时间复杂度:O(2^n)
- 空间复杂度:O(n) 最深递归
-
一开始没想到可以动态规划,还是题目做的少了。在阅读Github上的解法我才明白这道题和LCS问题是类似的。
观察模式能匹配时,必有模式的子模式匹配字符串的子串。
例如,aa*b 匹配 ab,必有aa*匹配a,必有 a 匹配 a。
令字符串 模式
假设 可以被模式 匹配
1.如果 匹配 ,则 匹配
2.如果 不匹配 , 则 匹配 且 匹配
3.如果 不匹配 ,则有 匹配 , 匹配
因此必有 可以被 匹配, 被 。
即具有最优子结构:问题的最优解(匹配)包含其子问题(子串与子模式)的最优解(匹配)
其次,在求解匹配组合方式时
匹配 有
可以发现若求 需要求其子序列的全组合,对于其子序列又要求子序列的全组合,包含大量重复构造。
即具有公共子问题:问题的子问题有共同的子问题
因此问题转化为对 m*n 矩阵dp[i][j] = P[:i]是否匹配S[:j]。- 时间复杂度:O(mn)
- 空间复杂度:O(mn)
代码
思路1:时间复杂度:O(3^n),空间复杂度:O(n)
def re_exp_matching(s, p):
"""
:type s: str for match
:type p: pattern str
:rtype: match or not
"""
def is_match(str, pattern):
if str >= len(s) and pattern >= len(p):
return True
if str < len(s) and pattern >= len(p):
return False
if pattern + 1 < len(p) and p[pattern + 1] == '*':
if str < len(s) and (p[pattern] == s[str] or p[pattern] == '.'):
return is_match(str + 1, pattern + 2) or is_match(str + 1, pattern) or is_match(str, pattern + 2)
# 1.move to next pattern(= stay and skip next time) 2. stay current pattern 3.skip
else:
return is_match(str, pattern + 2)
elif str < len(s) and (s[str] == p[pattern] or p[pattern] == '.'):
return is_match(str + 1, pattern + 1)
return False
return is_match(0,0)
思路2:时间复杂度:O(2^n),空间复杂度:O(n)
def re_exp_matching_backward(s, p):
def is_match(chr_for_match, match_pattern):
return match_pattern == '.' or match_pattern == chr_for_match
def match_core(str, pattern):
if pattern < 0:
return str < 0
if str < 0 :
if p[pattern] != '*':
return False
else:
return match_core(str, pattern - 2)
if p[pattern] == '*':
if is_match(s[str], p[pattern - 1]):
# continue match
if match_core(str - 1, pattern):
# key point: use match_core[pattern] to keep program stay in * pattern
# if False can backtrack
return True
# skip
return match_core(str, pattern - 2)
if is_match(s[str], p[pattern]):
return match_core(str - 1, pattern - 1)
return False
return match_core(len(s) - 1, len(p) -1)
思路3:时间复杂度:O(mn),空间复杂度:O(mn)
def re_exp_matching_dp(s, p):
# inital
col, row = len(p), len(s)
dp = [[0]* (col+1) for _ in range(row+1)]
dp[0][0] = 1
# init row-0
#if pattern can match '' @ head of string
for i in range(2, col+1):
if p[i-1] == '*':
dp[0][i] = dp[0][i-2]
for i in range(1, row + 1):
for j in range(1, col + 1):
if p[j-1] == '*':
if s[i-1] != p[j-2] and p[j-2] != '.':
# situation-skip see analysis 3.2
dp[i][j] = dp[i][j-2] # skip
elif s[i-1] == p[j-2] or p[j-2] == '.':
# situation see analysis 3.3
dp[i][j] = dp[i][j-2] or dp[i-1][j] # match or skip
elif s[i-1] == p[j-1] or p[j-1] == '.':
# non*-situation see analysis 3.1
dp[i][j] = dp[i-1][j-1] # match
return dp[-1][-1] == 1 # if s match p
思考
- 这道题还是挺难的,也是我学了最长公共子序列问题后遇到第一道【最优子结构+公共子问题】的问题。书是要看要学的,题目还是要多实践,不然很难讲题目的本质抽象出来。这次这题目从头分析花了不少时间,但是下次再做类似的题目思路就清晰了。
- 时间复杂度估算的重要性,强烈建议此题在Leetcode上测试:
- 书上思路,照抄:无法AC leetcode,修改后:1800ms
- 从后匹配:160ms
- 动态规划:76ms
Leetcode 10. 正则表达式匹配
题目
给定一个字符串 (s) 和一个字符模式 §。实现支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符。
‘*’ 匹配零个或多个前面的元素。
匹配应该覆盖整个字符串 (s) ,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
示例 1:
输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:
s = “aa”
p = “a*”
输出: true
解释: ‘*’ 代表可匹配零个或多个前面的元素, 即可以匹配 ‘a’ 。因此, 重复 ‘a’ 一次, 字符串可变为 “aa”。
示例 3:
输入:
s = “ab”
p = “."
输出: true
解释: ".” 表示可匹配零个或多个(’*’)任意字符(’.’)。
示例 4:
输入:
s = “aab”
p = “cab”
输出: true
解释: ‘c’ 可以不被重复, ‘a’ 可以被重复一次。因此可以匹配字符串 “aab”。
示例 5:
输入:
s = “mississippi”
p = “misisp*.”
输出: false