只把刷题过程中的重要步骤理解、重要代码理解、代码技巧做总结。毫无疑问,会反复刷多遍同样的题目,所以每次刷都会把增加的理解做更新。
总结按照不同的算法专题来展开,但是笔记可能会包含该题的其他类别算法。
class Solution(object):
def isMatch(self, text, pattern):
if not pattern:
return not text
first_match = bool(text) and pattern[0] in {text[0], '.'}
if len(pattern) >= 2 and pattern[1] == '*':
return (self.isMatch(text, pattern[2:]) or
first_match and self.isMatch(text[1:], pattern))
else:
return first_match and self.isMatch(text[1:], pattern[1:])
关于这里的if判断语句的理解:
可以看到不管此时的首位字符是否匹配,只要pattern的长度大于等于2且当前第二位的位置是“ * ” 就都进入了此判断的分支。 “ * ”表示的是有0个或者更多个其前面的元素,这就意味着“ * ”是不可能单独存在的,其在pattern中只有两种存在形式,第一种就是是“字母 * ”(比如a*, b*, c*),第二种就是“ . * ”。这时候如果首位字母匹配,那可能是x…(省略号表示x后面的其他字符,后同)和x匹配,也有可能是x…和.匹配;如果首位字母不匹配,则只有可能是x…和 “y”这种情况。我在想如果这里首字符是不匹配的,这时候text和pattern还有可能匹配吗?我觉得应该是不可能的,不管这时候的text和pattern是最先开始的完整字符串还是子字符串。比如text=“abbb”和pattern=“b”也能满足if分支的判断,但是无需再进行递归了,所以自认为可以将第一个分支的语句改写为:
if len(pattern) >= 2 and pattern[1] == '*':
return first_match and ( (self.isMatch(text, pattern[2:]) or
first_match and self.isMatch(text[1:], pattern) )
但是这里既然没有加上,说明原始代码里面的语句是同时适用于首字符匹配和不匹配的情况的,接下来具体分析。再仔细看一下代码,就会发现语句是由逻辑或连接的两个子语句,这说明只要其中的一个子语句成立,那么就是匹配的,特别地可以注意到第二个子语句中居然也加上了first_match, 所以这是怎么回事呢?首先我们分别看一下两个子语句所对应的子匹配问题,self.isMatch(text, pattern[2:]说的是将原始文本与匹配字符串的子串进行匹配,也就是说原始文本不需要和匹配字符串的前两个字符进行匹配了;self.isMatch(text[1:], pattern)说的是将原始文本的子串与完整的匹配字符串进行匹配。很容易发现,这两个匹配子问题是分别改动了匹配模式和待匹配文本。
我们试图找到具体例子来进行说明:
(1)text=“aaa”, pattern=“a*”,
前者pattern中只含有两个字符,所以self.isMatch(text, pattern[2:])就变成了非空text和空pattern的匹配,必然直接返回false,而first_match and self.isMatch(text[1:], pattern) ), 由于首字符相同first_match为true, self.isMatch(text[1:], pattern)变成了“aa”与“a*”匹配,继续递推下去很明显其等于“a”和“a*”匹配,再递推下去就变成了“”(空字符串)与“a*”匹配,再递推下去就变成了“”和“”匹配,毫无疑问这是可以匹配成果的,也是递推匹配的终点
之所以这样做是因为这里面对的匹配依赖于前面的字符,如果单纯的递归,子问题中就看不到前面的字符了,除非显示传递过去,但是这样将会变得非常麻烦。
因为代表是0个或者多个前面元素的重复,利用这个特性才有了这里的技巧。
考虑“a”和“a*”这样比较极端的例子,也就是代表前面元素的0重复,这时只用递推一次就变成了“”和“a”的匹配了,再递推一次就变成了“”和“”的匹配了,很明显匹配成功;而其他非极端情况,即代表至少有1次以上的前面元素的重复,如“aa”和“a”, “aaaa”和“a*”, “aaaaa”和“a*”等等最终都会递推到“a”和“a*”这种极端情况,也就是自然能够匹配了。
(2)text=“aaab”,pattern="ab"**
这个例子与上面的例子只有一点不同,就是pattern不止两个字符,这会让self.isMatch(text, pattern[2:]变得不一样,不再是非空字符串和空模式进行匹配了,这里是“aaab”和“b”进行匹配,很明显这时候if分支不满足,且因为首字母不匹配直接返回false。self.isMatch(text[1:], pattern)这时候是“aab”和“ab”匹配,“aab”和“b”匹配失败,然后是“ab”和“ab”匹配,“ab”和“b”匹配失败,然后是“b”和“a*b”匹配,b和b匹配成功。
**
这里面很容易认为first_match and self.isMatch(text[1:], pattern)就可以了,也就是第一个字符匹配后再判断余下的字符是否能匹配,但是问题在于ab和.*这个特色例子,如果直接使用这种方法,a和.能匹配,但是b和“ * ”不能匹配,但是实际上这个例子是可以匹配的。self.isMatch(text, pattern[2:])这种匹配看起来毫无意义,因为如果pattern的第二个字符中含有 * 的话,*的位置可以匹配0个字符,但是pattern的第一个字符必然得匹配text中的一个字符,也就是说pattern的前面两个字符是不可能匹配一个空的字符串的,那么直接把完整的字符串和pattern[2:]进行匹配自然也是不可能得到true的结果的。那么为啥要用这个呢?还是回到ab和.的例子上来,ab和‘’不匹配,a和‘.’匹配后再判断b和.是否能匹配,b和‘’不匹配,b和.匹配然后‘’和‘.’匹配,’'和‘’匹配。就是因为这个能够帮助‘b’和‘.’匹配.