刻意练习:LeetCode实战 -- Task30.通配符匹配

背景

本篇图文是LSGO软件技术团队组织的 第二期基础算法(Leetcode)刻意练习训练营 的打卡任务。本期训练营采用分类别练习的模式,即选择了五个知识点(数组、链表、字符串、树、贪心算法),每个知识点选择了 三个简单、两个中等、一个困难 等级的题目,共计三十道题,利用三十天的时间完成这组刻意练习。

本次任务的知识点:贪心算法

贪心算法(greedy algorithm),又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。

贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。

贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。


题目

  • 题号:44
  • 难度:困难
  • https://leetcode-cn.com/problems/wildcard-matching/

给定一个字符串(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

示例 6:

输入:
"abefcdgiescdfimde"
"ab*cd?i*de"
输出:true

示例 7:

输入:
"aaaa"
"***a"
输出:true

实现

第一种:双索引法

我们用ij分别标记sp的第一个字符下标,即都初始化为0。用istartjstart分别标记sp'*'匹配过的位置,即初始化为-1

和普通字符串匹配的思路差不多,已经匹配成功的部分就不再考虑了,所以要用ij标记当前正在比较的字符;但是最近匹配过的'*'可能会被重复使用去匹配更多的字符,所以我们要用istartjstart分别标记sp中最近匹配过'*'的位置。

  1. 如果ij标记的字符正好相等或者j字符是'?'匹配成功,则"移除"ij元素,即自增ij
  2. 否则如果j字符是'*'依然可以匹配成功,则用istartjstart分别标记i元素和j元素,自增j
  3. 再否则如果istart>-1说明之前匹配过'*',因为'*'可以匹配多个字符,所以这里要再次利用这个最近匹配过的'*'匹配更多的字符,移动i标记istart的下一个字符,再让istart重新标记i元素,同时移动j标记jstart的下一个字符。
  4. 上述三种情况都不满足,则匹配失败,返回false

最后当s中的字符都判断完毕,则认为s为空,此时需要p为空或者p中只剩下星号的时候,才能成功匹配。

  • 执行结果:通过
  • 执行用时:92 ms, 在所有 C# 提交中击败了 95.00% 的用户
  • 内存消耗:25.7 MB, 在所有 C# 提交中击败了 66.67% 的用户
public class Solution
{
    public bool IsMatch(string s, string p)
    {
        //若正则串p为空串,则s为空串匹配成功,s不为空串匹配失败。
        if (string.IsNullOrEmpty(p))
            return string.IsNullOrEmpty(s) ? true : false;

        int i = 0, j = 0, istart = -1, jstart = -1, plen = p.Length;

        //判断s的所有字符是否匹配
        while (i < s.Length)
        {
            //三种匹配成功情况以及匹配失败返回false
            if (j < plen && (s[i] == p[j] || p[j] == '?'))
            {
                i++;
                j++;
            }
            else if (j < plen && p[j] == '*')
            {
                istart = i;
                jstart = j;
                j++;
            }
            else if (istart > -1)
            {
                i = istart + 1;
                istart = i;
                j = jstart + 1;
            }
            else
            {
                return false;
            }
        }
        //s中的字符都判断完毕,则认为s为空
        //此时需要p为空或者p中只剩下星号的时候,才能成功匹配。
        //如果p中剩余的都是*,则可以移除剩余的*
        while (j < plen && p[j] == '*')
        {
            j++;
        }
        return j == plen;
    }
}

第二种:动态规划

dp数组的含义:dp[i,j]意思是s的前i个元素能否被p的前j个元素成功匹配。

知道了dp数组的含义之后,我们就知道了初始化细节:

  1. bool类型的dp数组,大小是[s.length+1,p.length+1],因为存在s前0个字符和p前0个字符的情况,即s为空串或p为空串。
  2. dp[0,0]一定是true,因为s空串和p空串是可以匹配成功的;dp[1,0] ~ dp[s.length,0]一定都是false,因为s不为空串而p为空串是不能匹配成功的。
  3. dp[0,1] ~ dp[0,p.length]当s为空串的时候,而p不是空串的时候,当且仅当pj字符以及前面都为'*'才为true
  4. dp[s.length,p.length]就得到了sp最终的匹配情况。

有了上述理解之后,就可以初始化dp数组了。

然后填写dp数组剩余部分即可,状态转移方程:

  1. s[i] == p[j]或者p[j] == '?',则dp[i,j] = dp[i-1,j-1]。可以理解为当前字符成功匹配后,只需要考虑之前的字符串是否匹配即可。
  2. p[j] == '*',则dp[i,j] = dp[i-1,j] || dp[i,j-1]。可以理解为当字符为'*'的时候会出现两种情况,第一种是'*'需要作为一个字母与s[i]进行匹配;第二种是'*'需要作为空字符(即不需要'*'可以直接"移除"),所以dp[i,j-1];用逻辑或将两种情况连接,是因为只要有一种情况可以匹配成功则当前匹配成功,有点暴力算法的感觉。
  3. 最后当s[i] !=p [j] && p[j] != '*'dp[i,j] = false。这步可以省略,因为dp数组元素的默认值就是false,所以不必要进行显式的赋值为false

有了上面的理解,我们就可以写代码了。

  • 执行结果:通过
  • 执行用时:112 ms, 在所有 C# 提交中击败了 62.50% 的用户
  • 内存消耗:28.6 MB, 在所有 C# 提交中击败了 22.22% 的用户
class Solution
{
    public bool IsMatch(string s, string p)
    {
        if (string.IsNullOrEmpty(p))
            return string.IsNullOrEmpty(s) ? true : false;

        int slen = s.Length, plen = p.Length;
        bool[,] dp = new bool[slen + 1, plen + 1];

        //初始化dp数组
        //dp[1][0]~dp[s.length][0]默认值flase不需要显式初始化为false
        dp[0, 0] = true;

        //dp[0][1]~dp[0][p.length]只有p的j字符以及前面所有字符都为'*'才为true
        for (int j = 1; j <= plen; j++)
        {
            dp[0, j] = p[j - 1] == '*' && dp[0, j - 1];
        }

        //填写dp数组剩余部分
        for (int i = 1; i <= slen; i++)
        {
            for (int j = 1; j <= plen; j++)
            {
                char si = s[i - 1], pj = p[j - 1];
                if (si == pj || pj == '?')
                {
                    dp[i, j] = dp[i - 1, j - 1];
                }
                else if (pj == '*')
                {
                    dp[i, j] = dp[i - 1, j] || dp[i, j - 1];
                }
            }
        }
        return dp[slen, plen];
    }
}

往期活动

LSGO软件技术团队会定期开展提升编程技能的刻意练习活动,希望大家能够参与进来一起刻意练习,一起学习进步!

  • Python基础刻意练习活动即将开启,你参加吗?
  • Task01:变量、运算符与数据类型
  • Task02:条件与循环
  • Task03:列表与元组
  • Task04:字符串与序列
  • Task05:函数与Lambda表达式
  • Task06:字典与集合
  • Task07:文件与文件系统
  • Task08:异常处理
  • Task09:else 与 with 语句
  • Task10:类与对象
  • Task11:魔法方法
  • Task12:模块

我是 终身学习者“老马”,一个长期践行“结伴式学习”理念的 中年大叔

我崇尚分享,渴望成长,于2010年创立了“LSGO软件技术团队”,并加入了国内著名的开源组织“Datawhale”,也是“Dre@mtech”、“智能机器人研究中心”和“大数据与哲学社会科学实验室”的一员。

愿我们一起学习,一起进步,相互陪伴,共同成长。

后台回复「搜搜搜」,随机获取电子资源!
欢迎关注,请扫描二维码:

你可能感兴趣的:(C#学习,数据结构与算法)