[剑指Offer] 19_正则表达式匹配

版权声明:Tian Run https://blog.csdn.net/u013908099/article/details/85954619

题目

请实现一个函数来匹配包含’.‘和’*‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串中的所有字符匹配整个模式。

例:

字符串"aaa"与模式"a.a"和“ab*ac*a">匹配,但与"aa.a"和"ab*a"均不匹配。


思路

  1. 正面暴力深度优先搜索。书上给出的解法。注意使用递归以便处理回溯。(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)。然而这里三个情况是有重叠的。下一个思路将会讲解。

    1. 时间复杂度:O(3^n)
    2. 空间复杂度:O(n) 最深递归
  2. 从后暴力深度优先搜索。因为*出现在字符后面,如果从前向后遍历,将会需要判断字符后一个是否为 * 。同时分析思路1,对于出现 * 时的策略,2-【此次s匹配下面移动向匹配p+2】,3-【此次不匹配s直接移动向下匹配p+2】,是同一种情况。因为当出现3时,即为选择1->2策略。所以对于 * 根本上只有2种处理,1-【继续匹配】2-【跳过】。
    此外,对于边界书上的代码只考虑 s 是否超界时,p是否超界,对于p未超界情况,未分开讨论,导致复杂度增加。

    1. 时间复杂度:O(2^n)
    2. 空间复杂度:O(n) 最深递归
  3. 一开始没想到可以动态规划,还是题目做的少了。在阅读Github上的解法我才明白这道题和LCS问题是类似的。
    观察模式能匹配时,必有模式的子模式匹配字符串的子串。
    例如,aa*b 匹配 ab,必有aa*匹配a,必有 a 匹配 a。
    令字符串 S = < s 1 , s 2 , s 3 , . . . , s m > S = <s_1, s_2, s_3, ...,s_m> 模式 P = < p 1 , p 2 , p 3 , . . . , p n > P = <p_1, p_2, p_3, ...,p_n>
    假设 Z = < s 1 , s 2 , s 3 , . . . , s k > ( k < m ) Z = <s_1, s_2, s_3, ...,s_k>(k<m) 可以被模式 X = < p 1 , p 2 , p 3 , . . . , p i > ( i < n ) X = <p_1, p_2, p_3, ...,p_i>(i<n) 匹配
    1.如果 p i p_i 匹配 s k s_k ,则 < p 1 , . . . , p i 1 > <p_1,...,p_{i-1}> 匹配 < s 1 , s 2 , s 3 , . . . , s k 1 > <s_1, s_2, s_3, ...,s_{k-1}>
    2.如果 p i p_i 不匹配 s k s_k , 则 p i p_i 匹配 < > <空字符> < p 1 , . . . , p i 1 > <p_1,...,p_{i-1}> 匹配 < s 1 , s 2 , s 3 , . . . , s k > <s_1, s_2, s_3, ...,s_{k}>
    3.如果 < p 1 , . . . , p i 1 > <p_1,...,p_{i-1}> 不匹配 < s 1 , s 2 , s 3 , . . . , s k > <s_1, s_2, s_3, ...,s_{k}> ,则有 < p 1 , . . . , p i 1 > <p_1,...,p_{i-1}> 匹配 < s 1 , s 2 , s 3 , . . . , s k l > <s_1, s_2, s_3, ...,s_{k-l}> < p i > <p_i> 匹配 < s k l + 1 , s 2 , s 3 , . . . , s k > <s_{k-l+1}, s_2, s_3, ...,s_{k}>
    因此必有 Z = < s 1 , s 2 , s 3 , . . . , s k j > Z=<s_1, s_2, s_3, ...,s_{k-j}> 可以被 p = < p 1 , . . . , p i l > p=<p_1,...,p_{i-l}> 匹配, < s k j + 1 , . . . , s k > <s_{k-j+1},...,s_k> < p i l + 1 , . . . , p i > <p_{i-l+1},...,p_i>
    即具有最优子结构:问题的最优解(匹配)包含其子问题(子串与子模式)的最优解(匹配)
    其次,在求解匹配组合方式时
    I { A } = { 0 , A = 0 1 , A > 0 I\{A\} = \left\{ \begin{aligned} 0 ,A=0 \\ 1 ,A>0 \\ \end{aligned} \right.

    Z Z 匹配 X X
    i s m a t c h ( Z k , X i ) = j = 1 k 1 l = 1 i 1 I ( i s m a t c h ( Z 1 j , X 1 j ) ) I ( i s m a t c h ( Z j + 1 k , X l + 1 i ) ) ismatch(Z_k, X_i) =\sum_{j=1}^ {k-1}\sum_{l=1}^ {i-1} I(ismatch(Z_1^j, X_1^j)) * I(ismatch(Z_{j+1}^k, X_{l+1}^i))
    可以发现若求 i s m a t c h Z k X i ismatch(Z_k,X_i) 需要求其子序列的全组合,对于其子序列又要求子序列的全组合,包含大量重复构造。
    即具有公共子问题:问题的子问题有共同的子问题
    因此问题转化为对 m*n 矩阵dp[i][j] = P[:i]是否匹配S[:j]。

    1. 时间复杂度:O(mn)
    2. 空间复杂度: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

思考

  1. 这道题还是挺难的,也是我学了最长公共子序列问题后遇到第一道【最优子结构+公共子问题】的问题。书是要看要学的,题目还是要多实践,不然很难讲题目的本质抽象出来。这次这题目从头分析花了不少时间,但是下次再做类似的题目思路就清晰了。
  2. 时间复杂度估算的重要性,强烈建议此题在Leetcode上测试:
    1. 书上思路,照抄:无法AC leetcode,修改后:1800ms
    2. 从后匹配:160ms
    3. 动态规划: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

猜你喜欢

转载自blog.csdn.net/u013908099/article/details/85954619
今日推荐