算法的重要性,我就很少说了吧,想去大厂,就必需要通过基础知识和业务逻辑面试+算法面试。因此,为了提升你们的算法能力,这个号后续天天带你们作一道算法题,题目就从LeetCode上面选 !面试
今天和你们聊的问题叫作正则表达式匹配,咱们先来看题面:正则表达式
Given an input string (s) and a pattern (p), implement regular expression
matching with support for '.' and '*'.算法
'.' Matches any single character.
'*' Matches zero or more of the preceding element.express
The matching should cover the entire input string (not partial).数组
https://leetcode.com/problems/regular-expression-matching/ide
Note:code
这道题属于典型的人狠话很少的问题,让咱们动手实现一个简单的正则匹配算法。不过为了下降难度,这里须要匹配的只有两个特殊符号,一个符号是'.',表示能够匹配任意的单个字符。还有一个特殊符号是'*',它表示它前面的符号能够是任意个,能够是0个。递归
题目要求是输入一个母串和一个模式串,请问是否可以达成匹配。ip
示例 1:输入:s = "aa"p = "a"输出: false解释: "a" 没法匹配 "aa" 整个字符串。示例 2:输入:s = "aa"p = "a*"输出: true解释: 由于 '*' 表明能够匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。所以,字符串 "aa" 可被视为 'a' 重复了一次。示例 3:输入:s = "ab"p = ".*"输出: true解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。示例 4:输入:s = "aab"p = "c*a*b"输出: true解释: 由于 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。所以能够匹配字符串 "aab"。示例 5:输入:s = "mississippi"p = "mis*is*p*."输出: false题解element
这题要求的是彻底匹配,而不是包含匹配。也就是说s串匹配完p串以后不能有剩余,好比恰好彻底匹配才行。明确了这点以后,咱们先来简化操做,假设不存在'*'这个特殊字符,只存在'.',那么显然,这个简化事后的问题很是简单,咱们随便就能够写出代码:
def match(s, p): n = len(s) for i in range(n): if s[i] == p[i] or p[i] == '.': continue return False return True
咱们下面考虑加入'*'的状况,其实加入'*'只会有一个问题,就是'*'能够匹配任意长度,若是当前位置出现了'*',咱们并不知道它应该匹配到哪里为止。咱们不知道须要匹配到哪里为止,那么就须要进行搜索了。也就是说,咱们须要将它转换成一个搜索问题来进行求解。咱们试着用递归来写一下:
def match(s, p, i, j): # 当前位置是否匹配 flag = s[i] == p[j] or p[j] == '.' # 判断p[j+1]是不是*,若是是那么说明p[j]能够跳过匹配 if j+1 < len(p) and p[j+1] == '*': # 两种状况,一种是跳过p[j],另外一种是p[j]继续匹配 return match(s, p, i, j+2) or (flag and match(s, p, i+1, j)) else: # 若是没有*,只有一种可能 return flag and match(s, p, i+1, j+1)
这段代码的精髓在于,因为'*'以前的符号也能够是0个,因此咱们不能判断当前位置是否会是'*',而要判断后面一个位置是不是'*'。这句话看起来有些像是绕口令,可是倒是这道题的精髓,若是看不懂的话,能够结合一下代码思考。总之,就是以出否出现'*'为基点,分状况进行递归便可。从代码上来看算上注释才12行,但是将这里面的关系都梳理清楚,并不容易。仍是很是考验基本功的,须要对递归有较深刻的理解才行。不过,这并非最好的方法,由于你会发现有不少状态被重复计算了不少次。这也是递归算法常常遇到的问题之一,要解决倒也不难,咱们很容易发现,对于固定的i和j,答案是固定的。那么,咱们能够用一个数组来存储全部的i和j的状况。若是当前的i和j处理过了,那么直接返回结果,不然再去计算。这种方法称做记忆化搜索,提及来复杂,可是实现起来只须要加几行代码:
memory = {}def match(s, p, i, j): if (i, j) in memory: return memory[(i, j)] # 当前位置是否匹配 flag = s[i] == p[j] or p[j] == '.' # 判断p[j+1]是不是*,若是是那么说明p[j]能够跳过匹配 if j+1 < len(p) and p[j+1] == '*': # 两种状况,一种是跳过p[j],另外一种是p[j]继续匹配 ret = match(s, p, i, j+2) or (flag and match(s, p, i+1, j)) else: # 若是没有*,只有一种可能 ret = flag and match(s, p, i+1, j+1) memory[(i, j)] = ret return ret
若是你对动态规划足够熟悉的话,想必也应该知道,记忆化搜索本质也是动态规划的一种实现方式。但一样,咱们也能够选择其余的方式实现动态规划,就能够摆脱递归了,相比于递归,使用数组存储状态的递推形式更容易理解。
咱们用dp[i][j]存储s[:i]与p[:j]是否匹配,那么根据咱们以前的结论,若是p[j-1]是'*',那么dp[i][j]可能由dp[i][j-2]或者是dp[i-1][j]转移获得。dp[i][j-2]比较容易想到,就是'*'前面的字符做废,为何是dp[i-1][j]呢?这种状况是表明'*'连续匹配,由于可能匹配任意个,因此必需要匹配在'*'这个位置。
举个例子:
s = 'aaaaa'
p = '.*'
在上面这个例子里,'.'能匹配全部字符,可是问题是s中只有一个a能匹配上。若是咱们不用dp[i-1][j]而用dp[i-1][j-1]的话,那么是没法匹配aa或者aaa这种状况的。由于这几种状况都是经过'*'的多匹配能力实现的。若是还不理解的同窗, 建议仔细梳理一下它们之间的关系。
咱们用数组的形式写出代码:
def is_match(s, p): # 为了防止超界,咱们从下标1开始 s = '$' + s p = '$' + p n, m = len(s), len(p) dp = [[False for _ in range(m)] for _ in range(n)]
dp[0][0] = True
# 须要考虑s空串匹配的状况 for i in range(n): for j in range(1, m): # 标记当前位置是否匹配,主要考虑s为空串的状况 match = True if i > 0 and (s[i] == p[j] or p[j] == '.') else False # 判断j位置是否为'*' if j > 1 and p[j] == '*': # 若是是,只有两种转移的状况,第一种表示略过前一个字符,第二种表示重复匹配 dp[i][j] = dp[i][j-2] or ((s[i] == p[j-1] or p[j-1] == '.') and dp[i-1][j]) else: # 若是不是,只有一种转移的可能 dp[i][j] = dp[i-1][j-1] and match return dp[n-1][m-1]
这题的代码并不长,算上注释也不过20行左右。可是当中对于题意的思考,以及对算法的运用很高,想要AC难度仍是不低的。但愿你们可以多多揣摩,理解其中的精髓,尤为是最后的动态规划解法,很是经典,推荐必定要弄懂。固然若是实在看不明白也没有关系,在之后的文章,我会给你们详细讲解动态规划算法。今天的文章就到这里。