leetcode题解之44. 通配符匹配

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。

两个字符串完全匹配才算匹配成功。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *

示例 1:

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:
s = "aa"
p = "*"
输出: true
解释: '*' 可以匹配任意字符串。

示例 3:

输入:
s = "cb"
p = "?a"
输出: false
解释: '?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。

示例 4:

输入:
s = "adceb"
p = "*a*b"
输出: true
解释: 第一个 '*' 可以匹配空字符串, 第二个 '*' 可以匹配字符串 "dce".

示例 5:

输入:
s = "acdcb"
p = "a*c?b"
输出: false

方法一:带记忆的递归

这里的第一个想法是递归,是一个较为简单的方法,但是如果输入的字符串过长会导致递归深度很大,因此比较耗时。

  • 如果字符串相等 p == s,返回 True
  • 如果 p == '*',返回 True
  • 如果 p 为空或 s 为空,返回 False
  • 若当前字符匹配,即 p[0] == s[0]p[0] == '?',然后比较下一个字符,返回 isMatch(s[1:], p[1:])
  • 如果当前的字符模式是一个星号 p[0] == '*',则有两种情况。
    • 星号没有匹配字符,因此答案是 isMatch(s, p[1:])
    • 星号匹配一个字符或更多字符,因此答案是 isMatch(s[1:], p)
    • p[0] != s[0],返回 False

在这里插入图片描述
这个算法由于时间限制,他没有通过所有的测试用例,因此必须进行优化,以下是可以做到的:

  1. 记忆。这是优化递归的标准方法。我们使用 (s, p) 作为键,使用匹配与不匹配作为布尔值。创建一个记忆的哈希映射。将所有已经检查的 (s, p) 保留在哈希映射中。这样,如果有任何重复的检查,只需查看哈希表,而不需再次进行计算。
  2. 清理输入数据,不管 a****bc**cc 中有多少个星号,它们都可以简化为 a*bc*cc。这样的清理有助于减少递归深度。

算法:

  • 清理输入数据用一个星号代替多个星号:p = remove_duplicate_stars(p)
  • 初始化记忆哈希表 dp
  • 返回 helper 函数,用清理后的输入作为参数。
  • helper(s,p)
    • 如果 (s,p) 已经计算过存储在 dp 中,则返回 dp 中的值。
    • 如果字符串相等 p == sp == '*',在 dp 添加 dp[(s, p)] = True
    • 反之如果 ps 为空,则 dp[(s, p)] = False
    • 反之如果当前字符匹配,即 p[0] == s[0]p[0] == '?',则继续检查下一个字符并 dp[(s, p)] = helper(s[1:], p[1:])
    • 反之如果当前字符模式是一个星号 p[0] == '*',则有两种情况,要么不匹配字符要么匹配一个或多个,符,则 dp[(s, p)] = helper(s, p[1:]) or helper(s, p[1:])
    • 反之如果 p[0] != s[0],则 dp[(s, p)] = False
    • 返回 dp[(s, p)]
class Solution:
    def remove_duplicate_stars(self, p):
        if p == '':
            return p
        p1 = [p[0],]
        for x in p[1:]:
            if p1[-1] != '*' or p1[-1] == '*' and x != '*':
                p1.append(x)
        return ''.join(p1) 
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">helper</span><span class="hljs-params">(self, s, p)</span>:</span>
    dp = self.dp
    <span class="hljs-keyword">if</span> (s, p) <span class="hljs-keyword">in</span> dp:
        <span class="hljs-keyword">return</span> dp[(s, p)]

    <span class="hljs-keyword">if</span> p == s <span class="hljs-keyword">or</span> p == <span class="hljs-string">'*'</span>:
        dp[(s, p)] = <span class="hljs-literal">True</span>
    <span class="hljs-keyword">elif</span> p == <span class="hljs-string">''</span> <span class="hljs-keyword">or</span> s == <span class="hljs-string">''</span>:
        dp[(s, p)] = <span class="hljs-literal">False</span>
    <span class="hljs-keyword">elif</span> p[<span class="hljs-number">0</span>] == s[<span class="hljs-number">0</span>] <span class="hljs-keyword">or</span> p[<span class="hljs-number">0</span>] == <span class="hljs-string">'?'</span>:
        dp[(s, p)] = self.helper(s[<span class="hljs-number">1</span>:], p[<span class="hljs-number">1</span>:])
    <span class="hljs-keyword">elif</span> p[<span class="hljs-number">0</span>] == <span class="hljs-string">'*'</span>:
        dp[(s, p)] = self.helper(s, p[<span class="hljs-number">1</span>:]) <span class="hljs-keyword">or</span> self.helper(s[<span class="hljs-number">1</span>:], p)
    <span class="hljs-keyword">else</span>:
        dp[(s, p)] = <span class="hljs-literal">False</span>

    <span class="hljs-keyword">return</span> dp[(s, p)]
    
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">isMatch</span><span class="hljs-params">(self, s, p)</span>:</span>
    p = self.remove_duplicate_stars(p)
    <span class="hljs-comment"># memorization hashmap to be used during the recursion</span>
    self.dp = {}
    <span class="hljs-keyword">return</span> self.helper(s, p)

复杂度分析

  • 时间复杂度:最好的情况下 O(min(S,P))\mathcal{O}(\min(S, P)),最坏的情况下是 O(2min(S,P/2))\mathcal{O}(2^{\min(S, P/2)})。其中 SSPP 指的是输入字符串和字符模式的长度。 最好的情况很明显,让我们估算最坏的情况。最耗时的递归是字符模式上的星号形成树的情况,将执行两个分支 helper(s, p[1:])helper(s[1:], p)。数据清理后字符模式中的最大星树为 P/2P/2,因此时间复杂度为 O(2min(S,P/2))\mathcal{O}(2^{\min(S, P/2)})
  • 空间复杂度:O(2min(S,P/2))\mathcal{O}(2^{\min(S, P/2)}),用来存储记忆哈希表和递归调用堆栈。

方法二:动态规划

上面的递归方法体现了当递归深度大的时候有多耗时,所以我们尝试一些更迭代的方法。

第一种方法中的记忆给出了尝试动态规划的想法。这个问题和编辑距离非常相似,所以我们在这里使用完全相同的方法。

我们的想法是将问题简化为简单的问题,例如,有一个字符串 adcebdk 和字符模式 *a*b?k,计算是否匹配 D = True/False。我们将输入字符串和字符模式的长度 p_lens_len 和是否匹配 D[p_len][s_len] 联系起来。

让我们进一步介绍 D[p_idx][s_idx]D[p_idx][s_idx] 代表的是字符模式中的第 p_idx 字符和输入字符串的第 s_idx 字符是否匹配。

在这里插入图片描述
如果字符相同或字符模式的字符为 ?,则

1D[pidx][sidx]=D[pidx1][sidx1](1)规则1:D[p_{idx}][s_{idx}] = D[p_{idx} - 1][s_{idx} - 1] \qquad (1)

在这里插入图片描述
如果字符模式的字符为星号且 D[p_idx - 1][s_idx - 1] = True,则:

  • 星号匹配完成。
  • 星号继续匹配更多的字符。

2D[pidx1][i]=True,isidx1(2)规则 2:D[p_{idx} - 1][i] = \textrm{True}, i \ge s_{idx} - 1 \qquad(2)

所以,每一步的计算是基于之前完成的计算完成的。

1 / 2

算法:

  • 初始化匹配表为 False 除了 D[0][0] = True
  • 使用规则 1 和规则 2 计算表格,最后返回 D[p_len][s_len] 作为答案。
class Solution:
    def isMatch(self, s, p):
        s_len = len(s)
        p_len = len(p)
    <span class="hljs-comment"># base cases</span>
    <span class="hljs-keyword">if</span> p == s <span class="hljs-keyword">or</span> p == <span class="hljs-string">'*'</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>
    <span class="hljs-keyword">if</span> p == <span class="hljs-string">''</span> <span class="hljs-keyword">or</span> s == <span class="hljs-string">''</span>:
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
    
    <span class="hljs-comment"># init all matrix except [0][0] element as False</span>
    d = [ [<span class="hljs-literal">False</span>] * (s_len + <span class="hljs-number">1</span>) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(p_len + <span class="hljs-number">1</span>)]
    d[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>] = <span class="hljs-literal">True</span>
    
    <span class="hljs-comment"># DP compute </span>
    <span class="hljs-keyword">for</span> p_idx <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, p_len + <span class="hljs-number">1</span>):
        <span class="hljs-comment"># the current character in the pattern is '*'</span>
        <span class="hljs-keyword">if</span> p[p_idx - <span class="hljs-number">1</span>] == <span class="hljs-string">'*'</span>:
            s_idx = <span class="hljs-number">1</span>
            <span class="hljs-comment"># d[p_idx - 1][s_idx - 1] is a string-pattern match </span>
            <span class="hljs-comment"># on the previous step, i.e. one character before.</span>
            <span class="hljs-comment"># Find the first idx in string with the previous math.</span>
            <span class="hljs-keyword">while</span> <span class="hljs-keyword">not</span> d[p_idx - <span class="hljs-number">1</span>][s_idx - <span class="hljs-number">1</span>] <span class="hljs-keyword">and</span> s_idx &lt; s_len + <span class="hljs-number">1</span>:
                s_idx += <span class="hljs-number">1</span>
            <span class="hljs-comment"># If (string) matches (pattern), </span>
            <span class="hljs-comment"># when (string) matches (pattern)* as well</span>
            d[p_idx][s_idx - <span class="hljs-number">1</span>] = d[p_idx - <span class="hljs-number">1</span>][s_idx - <span class="hljs-number">1</span>]
            <span class="hljs-comment"># If (string) matches (pattern), </span>
            <span class="hljs-comment"># when (string)(whatever_characters) matches (pattern)* as well</span>
            <span class="hljs-keyword">while</span> s_idx &lt; s_len + <span class="hljs-number">1</span>:
                d[p_idx][s_idx] = <span class="hljs-literal">True</span>
                s_idx += <span class="hljs-number">1</span>
        <span class="hljs-comment"># the current character in the pattern is '?'</span>
        <span class="hljs-keyword">elif</span> p[p_idx - <span class="hljs-number">1</span>] == <span class="hljs-string">'?'</span>:
            <span class="hljs-keyword">for</span> s_idx <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, s_len + <span class="hljs-number">1</span>): 
                d[p_idx][s_idx] = d[p_idx - <span class="hljs-number">1</span>][s_idx - <span class="hljs-number">1</span>] 
        <span class="hljs-comment"># the current character in the pattern is not '*' or '?'</span>
        <span class="hljs-keyword">else</span>:
            <span class="hljs-keyword">for</span> s_idx <span class="hljs-keyword">in</span> range(<span class="hljs-number">1</span>, s_len + <span class="hljs-number">1</span>): 
                <span class="hljs-comment"># Match is possible if there is a previous match</span>
                <span class="hljs-comment"># and current characters are the same</span>
                d[p_idx][s_idx] = \
                d[p_idx - <span class="hljs-number">1</span>][s_idx - <span class="hljs-number">1</span>] <span class="hljs-keyword">and</span> p[p_idx - <span class="hljs-number">1</span>] == s[s_idx - <span class="hljs-number">1</span>]  
                                                           
    <span class="hljs-keyword">return</span> d[p_len][s_len]


class Solution {
public boolean isMatch(String s, String p) {
int sLen = s.length(), pLen = p.length();

<span class="hljs-comment">// base cases</span>
<span class="hljs-keyword">if</span> (p.equals(s) || p.equals(<span class="hljs-string">"*"</span>)) <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
<span class="hljs-keyword">if</span> (p.isEmpty() || s.isEmpty()) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;

<span class="hljs-comment">// init all matrix except [0][0] element as False</span>
<span class="hljs-keyword">boolean</span>[][] d = <span class="hljs-keyword">new</span> <span class="hljs-keyword">boolean</span>[pLen + <span class="hljs-number">1</span>][sLen + <span class="hljs-number">1</span>];
d[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>] = <span class="hljs-keyword">true</span>;

<span class="hljs-comment">// DP compute</span>
<span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> pIdx = <span class="hljs-number">1</span>; pIdx &lt; pLen + <span class="hljs-number">1</span>; pIdx++) {
  <span class="hljs-comment">// the current character in the pattern is '*'</span>
  <span class="hljs-keyword">if</span> (p.charAt(pIdx - <span class="hljs-number">1</span>) == <span class="hljs-string">'*'</span>) {
    <span class="hljs-keyword">int</span> sIdx = <span class="hljs-number">1</span>;
    <span class="hljs-comment">// d[p_idx - 1][s_idx - 1] is a string-pattern match</span>
    <span class="hljs-comment">// on the previous step, i.e. one character before.</span>
    <span class="hljs-comment">// Find the first idx in string with the previous math.</span>
    <span class="hljs-keyword">while</span> ((!d[pIdx - <span class="hljs-number">1</span>][sIdx - <span class="hljs-number">1</span>]) &amp;&amp; (sIdx &lt; sLen + <span class="hljs-number">1</span>)) sIdx++;
    <span class="hljs-comment">// If (string) matches (pattern),</span>
    <span class="hljs-comment">// when (string) matches (pattern)* as well</span>
    d[pIdx][sIdx - <span class="hljs-number">1</span>] = d[pIdx - <span class="hljs-number">1</span>][sIdx - <span class="hljs-number">1</span>];
    <span class="hljs-comment">// If (string) matches (pattern),</span>
    <span class="hljs-comment">// when (string)(whatever_characters) matches (pattern)* as well</span>
    <span class="hljs-keyword">while</span> (sIdx &lt; sLen + <span class="hljs-number">1</span>) d[pIdx][sIdx++] = <span class="hljs-keyword">true</span>;
  }
  <span class="hljs-comment">// the current character in the pattern is '?'</span>
  <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (p.charAt(pIdx - <span class="hljs-number">1</span>) == <span class="hljs-string">'?'</span>) {
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> sIdx = <span class="hljs-number">1</span>; sIdx &lt; sLen + <span class="hljs-number">1</span>; sIdx++)
      d[pIdx][sIdx] = d[pIdx - <span class="hljs-number">1</span>][sIdx - <span class="hljs-number">1</span>];
  }
  <span class="hljs-comment">// the current character in the pattern is not '*' or '?'</span>
  <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> sIdx = <span class="hljs-number">1</span>; sIdx &lt; sLen + <span class="hljs-number">1</span>; sIdx++) {
      <span class="hljs-comment">// Match is possible if there is a previous match</span>
      <span class="hljs-comment">// and current characters are the same</span>
      d[pIdx][sIdx] = d[pIdx - <span class="hljs-number">1</span>][sIdx - <span class="hljs-number">1</span>] &amp;&amp;
              (p.charAt(pIdx - <span class="hljs-number">1</span>) == s.charAt(sIdx - <span class="hljs-number">1</span>));
    }
  }
}
<span class="hljs-keyword">return</span> d[pLen][sLen];

}
}

复杂度分析

  • 时间复杂度:O(SP)\mathcal{O}(S P),其中 SSPP 指的是字符模式和输入字符串的长度。
  • 空间复杂度:O(SP)\mathcal{O}(S P),用来存储匹配表格。

方法三:回溯

复杂度 O(SP)\mathcal{O}(S P)O(2min(S,P/2))\mathcal{O}(2^{\min(S, P/2)}) 好的多,但是仍然有改进的余地。不需要计算整个表格,也就是检查每个星号的所有可能性:

  • 匹配 0 个字符。
  • 匹配 1 个字符。
  • 匹配 2 个字符。

...

  • 匹配所有剩余的字符。

让我们从匹配 0 个字符开始,如果这个假设导致不匹配,则回溯:回到前一个星号,假设它匹配一个字符,然后继续。若又是不匹配的情况?再次回溯:回到上一个星号,假设匹配两个字符,等等。

在这里插入图片描述

算法:

我们使用两个指针:s_idx 遍历输入字符串,p_idx 遍历字符模式。当 s_idx < s_len

  • 如果字符模式仍有字符 p_idx < p_len 且指针下的字符匹配 p[p_idx] == s[s_idx]p[p_idx] == '?',则两个指针向前移动。
  • 反之如果字符模式仍有字符 p_idx < p_lenp[p_idx] == '*',则首先检查匹配 0 字符的情况,即只增加模式指针 p_idx++。记下可能回溯的位置 star_idx 和当前字符串的位置 s_tmp_idx
  • 反之如果出现不匹配的情况:
    • 如果字符模式中没有星号,则返回 False
    • 如果有星号,则回溯:设置 p_idx = star_idx + 1s_idx = s_tmp_idx + 1,假设这次的星匹配多个字符。则可能的回溯为 s_tmp_idx = s_idx
  • 如果字符模式的所有剩余字符都是星号,则返回 True
class Solution:
    def isMatch(self, s, p):
        """
        :type s: str
        :type p: str
        :rtype: bool
        """
        s_len, p_len = len(s), len(p)
        s_idx = p_idx = 0
        star_idx = s_tmp_idx = -1
    <span class="hljs-keyword">while</span> s_idx &lt; s_len:
        <span class="hljs-comment"># If the pattern caracter = string character</span>
        <span class="hljs-comment"># or pattern character = '?'</span>
        <span class="hljs-keyword">if</span> p_idx &lt; p_len <span class="hljs-keyword">and</span> p[p_idx] <span class="hljs-keyword">in</span> [<span class="hljs-string">'?'</span>, s[s_idx]]:
            s_idx += <span class="hljs-number">1</span>
            p_idx += <span class="hljs-number">1</span>
        <span class="hljs-comment"># If pattern character = '*'</span>
        <span class="hljs-keyword">elif</span> p_idx &lt; p_len <span class="hljs-keyword">and</span> p[p_idx] == <span class="hljs-string">'*'</span>:
            <span class="hljs-comment"># Check the situation</span>
            <span class="hljs-comment"># when '*' matches no characters</span>
            star_idx = p_idx
            s_tmp_idx = s_idx
            p_idx += <span class="hljs-number">1</span>
        <span class="hljs-comment"># If pattern character != string character</span>
        <span class="hljs-comment"># or pattern is used up</span>
        <span class="hljs-comment"># and there was no '*' character in pattern </span>
        <span class="hljs-keyword">elif</span> star_idx == <span class="hljs-number">-1</span>:
            <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
        <span class="hljs-comment"># If pattern character != string character</span>
        <span class="hljs-comment"># or pattern is used up</span>
        <span class="hljs-comment"># and there was '*' character in pattern before</span>
        <span class="hljs-keyword">else</span>:
            <span class="hljs-comment"># Backtrack: check the situation</span>
            <span class="hljs-comment"># when '*' matches one more character</span>
            p_idx = star_idx + <span class="hljs-number">1</span>
            s_idx = s_tmp_idx + <span class="hljs-number">1</span>
            s_tmp_idx = s_idx
    
    <span class="hljs-comment"># The remaining characters in the pattern should all be '*' characters</span>
    <span class="hljs-keyword">return</span> all(x == <span class="hljs-string">'*'</span> <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> p[p_idx:])


class Solution {
public boolean isMatch(String s, String p) {
int sLen = s.length(), pLen = p.length();
int sIdx = 0, pIdx = 0;
int starIdx = -1, sTmpIdx = -1;

<span class="hljs-keyword">while</span> (sIdx &lt; sLen) {
  <span class="hljs-comment">// If the pattern caracter = string character</span>
  <span class="hljs-comment">// or pattern character = '?'</span>
  <span class="hljs-keyword">if</span> (pIdx &lt; pLen &amp;&amp; (p.charAt(pIdx) == <span class="hljs-string">'?'</span> || p.charAt(pIdx) == s.charAt(sIdx))){
    ++sIdx;
    ++pIdx;
  }
  <span class="hljs-comment">// If pattern character = '*'</span>
  <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (pIdx &lt; pLen &amp;&amp; p.charAt(pIdx) == <span class="hljs-string">'*'</span>) {
    <span class="hljs-comment">// Check the situation</span>
    <span class="hljs-comment">// when '*' matches no characters</span>
    starIdx = pIdx;
    sTmpIdx = sIdx;
    ++pIdx;
  }
  <span class="hljs-comment">// If pattern character != string character</span>
  <span class="hljs-comment">// or pattern is used up</span>
  <span class="hljs-comment">// and there was no '*' character in pattern </span>
  <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (starIdx == -<span class="hljs-number">1</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
  }
  <span class="hljs-comment">// If pattern character != string character</span>
  <span class="hljs-comment">// or pattern is used up</span>
  <span class="hljs-comment">// and there was '*' character in pattern before</span>
  <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Backtrack: check the situation</span>
    <span class="hljs-comment">// when '*' matches one more character</span>
    pIdx = starIdx + <span class="hljs-number">1</span>;
    sIdx = sTmpIdx + <span class="hljs-number">1</span>;
    sTmpIdx = sIdx;
  }
}

<span class="hljs-comment">// The remaining characters in the pattern should all be '*' characters</span>
<span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i = pIdx; i &lt; pLen; i++)
  <span class="hljs-keyword">if</span> (p.charAt(i) != <span class="hljs-string">'*'</span>) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
<span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;

}
}

复杂度分析

  • 时间复杂度:最好的情况下是 O(min(S,P))\mathcal{O}(\min(S, P)),平均情况下是 O(SlogP)\mathcal{O}(S \log P),其中 SSPP 指的是字符模式和输入字符串的长度。详细证明可点击:证明过程
  • 空间复杂度:O(1)\mathcal{O}(1)

https://www.jianshu.com/p/3bb840379b0d

posted @ 2020-06-28 23:58  刷题之路1  阅读(325)  评论(0)    收藏  举报