• 44、通配符匹配


    题目: 给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。

    • ‘?’ 可以匹配任何单个字符。
    • ‘*’ 可以匹配任意字符串(包括空字符串)。
    • 两个字符串完全匹配才算匹配成功。

    说明:

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

    方法一:动态规划

      在给定的模式 p 中,只会有三种类型的字符出现,其中「小写字母」和「问号」的匹配是确定的,而「星号」的匹配是不确定的,因此我们需要枚举所有的匹配情况。为了减少重复枚举,我们可以使用动态规划来解决本题。
      我们用 dp[i][j] 表示字符串 s 的前 i 个字符和模式 p 的前 j 个字符是否能匹配。在进行状态转移时,我们可以考虑模式 p 的第 j 个字符 p(j),与之对应的是字符串 s 中的第 i 个字符 s(i):

    如果 p(j)是小写字母,那么 s(i)必须也为相同的小写字母,状态转移方程为:

    • dp[i][j] = ( s (i)与 p (j)相同)∧dp[i−1][j−1]
      其中 ∧ 表示逻辑与运算。也就是说,dp[i][j]为真,当且仅当 dp[i-1][j-1]为真,并且 s (i) 与 p (j) 相同。

    如果 p (j)是问号,那么对 s (i)没有任何要求,状态转移方程为:

    • dp[i][j]=dp[i−1][j−1]

    如果 p (j)是星号,那么同样对 s (i)没有任何要求,但是星号可以匹配零或任意多个小写字母,因此状态转移方程分为两种情况,即使用或不使用这个星号:

    • dp[i][j]=dp[i][j−1]∨dp[i−1][j]
      其中 ∨ 表示逻辑或运算。如果我们不使用这个星号,那么就会从 dp[i][j-1] 转移而来;如果我们使用这个星号,那么就会从 dp[i-1][j]转移而来。

    因为当 s 的前 i 个字符和匹配字符串的 j-1 个字符已经匹配的时候,若p的第 j 个字符为星号,则 p 的前 j 个字符和 s 的前 i 个字符仍然一定是匹配的,这时候星号相当于匹配了空串,所以不使用星号的时候只要dp[i][j-1] 是匹配的,则dp[i][j] 一定是匹配的。同理当 p 的第 j 个字符为星号,而此时 p 的前 j-1 个字符不能和 s 的前 i 个字符匹配的时候,就需要用星号来匹配 s 中的部分字符,这时候就判断dp[i-1][j]是否匹配,若匹配的话星号即可相当于匹配了 其原匹配值加上s 的第 i 个字符 ,因为星号可以匹配字符串(前面可能还匹配了 s 的其他字符),这样dp[i][j] 仍然是匹配的。

    最终的状态转移方程如下:
    在这里插入图片描述
    细节
      只有确定了边界条件,才能进行动态规划。在上述的状态转移方程中,由于 dp[i][j] 对应着 s 的前 i 个字符和模式 p 的前 j 个字符,因此所有的 dp[0][j] 和 dp[i][0] 都是边界条件,因为它们涉及到空字符串或者空模式的情况,这是我们在状态转移方程中没有考虑到的:

    • dp[0][0]=True,即当字符串 s 和模式 p 均为空时,匹配成功

    • dp[i][0]=False,即空模式无法匹配非空字符串

    • dp[0][j] 需要分情况讨论:因为星号才能匹配空字符串,所以只有当模式 p 的前 j 个字符均为星号时,dp[0][j] 才为真

      我们可以发现,dp[i][0] 的值恒为假,dp[0][j] 在 j 大于模式 p 的开头出现的星号字符个数之后,值也恒为假,而 dp[i][j] 的默认值(其它情况)也为假,因此在对动态规划的数组初始化时,我们就可以将所有的状态初始化为 False,减少状态转移的代码编写难度。

      最终的答案即为 dp[m][n],其中 m 和 n 分别是字符串 s 和模式 p 的长度。需要注意的是,由于大部分语言中字符串的下标从 0 开始,因此 s (i)和 p (j)分别对应着 s[i-1]和 p[j-1]

    public boolean isMatch2(String s, String p) {
            int m = s.length();
            int n = p.length();
            boolean[][] dp = new boolean[m + 1][n + 1];
            dp[0][0] = true;
            //判断是否为全星号
            for (int i = 1; i <= n; ++i) {
                if (p.charAt(i - 1) == '*') {
                    dp[0][i] = true;
                } else {
                    break;
                }
            }
            //动态规划求每个位置是否匹配
            for (int i = 1; i <= m; ++i) {
                for (int j = 1; j <= n; ++j) {
                    if (p.charAt(j - 1) == '*') {
                        dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
                    } else if (p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) {
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                }
            }
            return dp[m][n];
        }
    

    时间复杂度:O(mn),其中 m 和 n 分别是字符串 s 和模式 p 的长度。
    空间复杂度:O(mn),即为存储所有 (m+1)(n+1) 个状态需要的空间。此外,在状态转移方程中,由于 dp[i][j]只会从 dp[i][…]以及 dp[i - 1][…]转移而来,因此我们可以使用滚动数组对空间进行优化,即用两个长度为 n+1的一维数组代替整个二维数组进行状态转移,空间复杂度为 O(n)

    例:p=“a*b?b” , s=“adcbeb”
    代码执行后dp的状态
    在这里插入图片描述

    方法二:贪心算法

      方法一的瓶颈在于对星号 ∗ 的处理方式:使用动态规划枚举所有的情况。由于星号是「万能」的匹配字符,连续的多个星号和单个星号实际上是等价的,那么不连续的多个星号呢?

      我们以 p= ∗ abcd ∗ 为例,p 可以匹配所有包含子串 abcd 的字符串,也就是说,我们只需要暴力地枚举字符串 s 中的每个位置作为起始位置,并判断对应的子串是否为 abcd 即可。这种暴力方法的时间复杂度为 O(mn),与动态规划一致,但不需要额外的空间。

      如果 p=∗ abcd∗efgh∗i ∗ 呢?显然,p 可以匹配所有依次出现子串 abcd、efgh、i 的字符串。此时,对于任意一个字符串 s,我们首先暴力找到最早出现的 abcd,随后从下一个位置开始暴力找到最早出现的 efgh,最后找出 i,就可以判断 s 是否可以与 p 匹配。这样「贪心地」找到最早出现的子串是比较直观的,因为如果 s 中多次出现了某个子串,那么我们选择最早出现的位置,可以使得后续子串能被找到的机会更大。

      因此,如果模式 p 的形式为 * U1* U2* U3* …* Ux*,即字符串(可以为空)和星号交替出现,并且首尾字符均为星号,那么我们就可以设计出下面这个基于贪心的暴力匹配算法。算法的本质是:如果在字符串 s 中首先找到 U1,再找到 U2, U3,⋯,Ux,那么 s 就可以与模式 p 匹配。

    然而模式 p 并不一定是 * U1* U2* U3* …* Ux* 的形式:

    • 模式 p 的开头字符不是星号;

    • 模式 p 的结尾字符不是星号。

      第二种情况处理起来并不复杂。如果模式 p 的结尾字符不是星号,那么就必须与字符串 s 的结尾字符匹配。那么我们不断地匹配 s 和 p 的结尾字符,直到 p 为空或者 p 的结尾字符是星号为止。在这个过程中,如果匹配失败,或者最后 p 为空但 s 不为空,False。

      第一种情况的处理也很类似,我们可以不断地匹配 s 和 p 的开头字符。下面的代码中给出了另一种处理方法,即修改 sRecord 和 pRecord 的初始值为 -1,表示模式 p 的开头字符不是星号,并且在匹配失败时进行判断,如果它们的值仍然为 −1,说明没有「反悔」重新进行匹配的机会。

    //方法二:贪心算法
        public boolean isMatch(String s, String p) {
            int sRight = s.length(), pRight = p.length();
            //模式p的结尾不是*号,为?或字符,则与s的结尾一个个判断是否匹配
            while (sRight > 0 && pRight > 0 && p.charAt(pRight - 1) != '*') {
                if (charMatch(s.charAt(sRight - 1), p.charAt(pRight - 1))) {
                    --sRight;
                    --pRight;
                } else {
                    //只要结尾有一个非*字符不能匹配s对应位置字符就返回false
                    return false;
                }
            }
            //p为空模式
            if (pRight == 0) {
                //s为空串则匹配,否则不匹配
                return sRight == 0;
            }
    
            int sIndex = 0, pIndex = 0;
            int sRecord = -1, pRecord = -1;
    
            while (sIndex < sRight && pIndex < pRight) {
            	//出现 * 假设其不匹配任何字符,继续判断剩下的 s 和 p 是否匹配
            	//并记录该 * 号下一个字符的下标,和对应的 s 的下标,便于(后悔)即不匹配的时候,返回来让 * 匹配部分 s 中的字符看剩下的是否匹配
                if (p.charAt(pIndex) == '*') {
                    ++pIndex;
                    sRecord = sIndex;
                    pRecord = pIndex;
                    //s[sIndex]和p[pIndex]匹配,则指针后移,判断剩下的是否匹配
                } else if (charMatch(s.charAt(sIndex), p.charAt(pIndex))) {
                    ++sIndex;
                    ++pIndex;
                    // 出现了 * 号,且 * 不起作用时(即不匹配任何字符串时),s 和 p 不匹配则倒退回来(后悔),
                    // 让 * 代表部分 s 的字符串,继续判断剩下的是否匹配
                } else if (sRecord != -1 && sRecord + 1 < sRight) {
                    ++sRecord;
                    sIndex = sRecord;
                    pIndex = pRecord;
                } else {
                    return false;
                }
            }
    
            return allStars(p, pIndex, pRight);
        }
        //若 p 的左边一部分已经完全匹配了 s ,则此时 pIndex < pRight, p 剩下的p(pIndex)到p(pRight) 部分必须全为*,才匹配
        //不然 s 不存在对应的字符串片段与 p 该段的字符相匹配,所以返回false
    	//如果最后pIndex==pRight则返回true,是匹配的
        public boolean allStars(String str, int left, int right) {
            for (int i = left; i < right; ++i) {
                if (str.charAt(i) != '*') {
                    return false;
                }
            }
            return true;
        }
    	//判断;两个字符是否匹配
        public boolean charMatch(char u, char v) {
            return u == v || v == '?';
    
        }
    

    时间复杂度:

    • 渐进复杂度:O(mn),其中 m 和 n 分别是字符串 s 和模式 p 的长度。从代码中可以看出,s[sIndex] 和 p[pIndex] 至多只会被匹配(判断)一次,因此渐进时间复杂度为 O(mn)。
    • 平均复杂度:O(mlogn)。

    空间复杂度:O(1)。

    参考:
    Leetcode官方题解

  • 相关阅读:
    .NET牛人应该知道些什么
    ASP.NET常用的分页
    获取字符串长度
    去掉网页的滚动条
    GridView动态生成字段常见问题及解决方法
    PagesSection.ValidateRequest 属性
    我刚刚做了一个艰难的决定
    Jquery UI Dialog 对话框学习
    c语言病毒分析(转)
    不用不熟悉的工具和方法
  • 原文地址:https://www.cnblogs.com/firecode7/p/16120435.html
Copyright © 2020-2023  润新知