题目
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
- ‘.’ 匹配任意单个字符
- ‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
示例
示例一
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例二
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例三
输入:s = "ab", p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
思路及算法代码
思路
-
初始化指针: 首先,通过 i 和 j 分别初始化字符串 s 和正则表达式 p 的指针,它们分别指向各自的最后一个字符。这是因为在匹配过程中,我们希望从字符串和正则表达式的末尾开始向前匹配。
-
循环匹配: 进入一个循环,直到匹配完成或者判断不匹配为止。循环中的每一步都是根据当前字符以及前面的匹配情况来决定如何移动指针和记录回溯位置。
-
匹配处理: 在循环中,根据当前字符以及正则表达式的规则进行匹配处理。如果当前字符与正则表达式字符匹配,或者正则表达式字符是通配符 .,则将字符串和正则表达式的指针分别向前移动一位。
-
特殊情况处理: 当遇到正则表达式中的 * 符号时,表示可能存在重复字符匹配,需要特殊处理。在代码中,我们先检查 * 前是否有字符,如果有,则尝试将 * 匹配 0 个字符,如果可以匹配,则记录当前回溯位置,并将字符串指针向前移动一位。如果不匹配,则直接跳过 * 及其前一个字符,继续向前匹配。
-
回溯处理: 如果在匹配过程中发现不匹配的情况,且存在回溯位置,则回溯到上一个记录的位置,并尝试其他的匹配方式。在代码中,通过 markj 和 marki 列表来记录回溯位置。
-
匹配成功: 当正则表达式扫描完毕后,如果字符串也扫描完毕,则表示匹配成功,返回 True;如果字符串未扫描完毕,则返回 False,表示不匹配。
-
循环结束: 当正则表达式扫描完毕且匹配成功时,循环结束,返回 True;当字符串扫描完毕但正则表达式未扫描完毕时,返回 False,表示不匹配。
代码
class Solution:def isMatch(self, s: str, p: str) -> bool:i = len(s)-1 # 初始化字符串指针,指向最后一个字符j = len(p)-1 # 初始化正则表达式指针,指向最后一个字符markj = [] # 用于记录回溯位置的正则表达式指针marki = [] # 用于记录回溯位置的字符串指针while True: # 进入无限循环if j < 0: # 如果正则表达式扫描完毕if i >= 0: # 但字符串未扫描完毕,返回不匹配return Falseelse: # 字符串也扫描完毕,匹配成功,退出循环break# 如果当前字符匹配或者是通配符'.'if i >= 0 and (p[j] == s[i] or p[j] == '.'):j -= 1 # 向前移动正则表达式指针i -= 1 # 向前移动字符串指针# 如果当前字符是'*'elif p[j] == '*':if j > 0: # 如果'*'前有字符if i >= 0 and (p[j-1] == s[i] or p[j-1] == '.'):markj.append(j) # 记录当前位置到回溯列表中marki.append(i)i -= 1 # 向前移动字符串指针,相当于'*'匹配了当前字符else:j -= 2 # 向前移动正则表达式指针,跳过'*'及其前一个字符else:j -= 2 # 向前移动正则表达式指针,跳过'*'及其前一个字符# 如果当前字符不匹配且没有回溯位置else:if len(markj) > 0: # 如果存在回溯位置j = markj.pop() # 回溯到上一个记录的位置i = marki.pop()j -= 2 # 向前移动正则表达式指针,跳过'*'及其前一个字符continuereturn False # 返回不匹配return True # 循环结束,匹配成功,返回True
复杂度分析
在这个算法中,我们使用了一个 while 循环来扫描字符串和正则表达式。在每次循环中,我们进行了一系列的比较和移动操作,而每个操作都是常数时间复杂度的。因此,总体时间复杂度主要取决于循环的次数。
设字符串的长度为 n,正则表达式的长度为 m。在最坏情况下,每次循环中,我们可能需要向前移动字符串指针和正则表达式指针,直到其中一个指针达到开头。因此,总体时间复杂度为 O(n + m)。
在空间复杂度方面,我们使用了两个列表 markj 和 marki 来记录回溯位置,它们的空间复杂度与回溯次数相关。在最坏情况下,每个字符都可能需要回溯。
回溯的次数受到两个因素的影响:
- 字符串 s 和正则表达式 p 的长度。回溯的次数取决于较短的字符串。
- 正则表达式 p 中的 * 符号的数量。每个 * 符号都可能导致一次回溯。
因此空间复杂度为 O(min(n, m))。
综上所述,该算法的时间复杂度为 O(n + m),空间复杂度为 O(min(n, m))。
知识点
回溯算法
回溯算法是一种通过不断尝试可能的解决方案来解决问题的策略。它在解空间中搜索问题的所有可能解,通过逐步向前移动并尝试不同的选择,直到找到解决方案或者确定无解。
回溯算法通常用于解决组合优化问题,例如排列、组合、子集等问题,以及搜索问题的解空间,如图搜索、迷宫求解等。
下面是回溯算法的一般步骤:
-
选择路径: 从问题的初始状态开始,根据问题的限制和条件,选择一条路径前进。这可以是在问题的解空间中的任何可能的选择。
-
尝试选择: 将选择应用到当前状态,改变当前状态,并继续向前探索。
-
检查约束条件: 检查当前状态是否满足问题的约束条件和限制。如果满足,则继续向前探索;否则,回溯到上一个状态,并尝试其他的选择。
-
终止条件: 当达到问题的终止条件时,即找到一个解决方案,或者搜索完整个解空间后仍未找到解决方案,算法终止。
-
回溯: 如果当前路径无法找到解决方案,或者达到某个限制条件,算法将回溯到上一个状态,撤销上一次的选择,并尝试其他的选择。
-
重复搜索: 重复上述步骤,直到找到解决方案或者搜索完整个解空间。
回溯算法的特点是在搜索过程中进行了深度优先遍历,而且不断回溯到之前的状态,尝试其他的可能性。这种方法虽然可以找到解决方案,但在某些情况下可能会需要大量的时间和空间,因此需要合理地设计问题和剪枝策略,以提高算法的效率。