本篇图文是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
第一种:双索引法
我们用i
和j
分别标记s
和p
的第一个字符下标,即都初始化为0。用istart
和jstart
分别标记s
和p
中'*'
匹配过的位置,即初始化为-1
。
和普通字符串匹配的思路差不多,已经匹配成功的部分就不再考虑了,所以要用i
和j
标记当前正在比较的字符;但是最近匹配过的'*'
可能会被重复使用去匹配更多的字符,所以我们要用istart
和jstart
分别标记s
和p
中最近匹配过'*'
的位置。
i
和j
标记的字符正好相等或者j
字符是'?'
匹配成功,则"移除"i
和j
元素,即自增i
、j
。j
字符是'*'
依然可以匹配成功,则用istart
和jstart
分别标记i
元素和j
元素,自增j
。istart>-1
说明之前匹配过'*'
,因为'*'
可以匹配多个字符,所以这里要再次利用这个最近匹配过的'*'
匹配更多的字符,移动i
标记istart
的下一个字符,再让istart
重新标记i
元素,同时移动j
标记jstart
的下一个字符。false
。最后当s
中的字符都判断完毕,则认为s
为空,此时需要p
为空或者p
中只剩下星号的时候,才能成功匹配。
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
数组的含义之后,我们就知道了初始化细节:
bool
类型的dp
数组,大小是[s.length+1,p.length+1]
,因为存在s
前0个字符和p
前0个字符的情况,即s
为空串或p
为空串。dp[0,0]
一定是true
,因为s
空串和p
空串是可以匹配成功的;dp[1,0] ~ dp[s.length,0]
一定都是false
,因为s
不为空串而p
为空串是不能匹配成功的。dp[0,1] ~ dp[0,p.length]
当s为空串的时候,而p
不是空串的时候,当且仅当p
的j
字符以及前面都为'*'
才为true
。dp[s.length,p.length]
就得到了s
和p
最终的匹配情况。有了上述理解之后,就可以初始化dp
数组了。
然后填写dp
数组剩余部分即可,状态转移方程:
s[i] == p[j]
或者p[j] == '?'
,则dp[i,j] = dp[i-1,j-1]
。可以理解为当前字符成功匹配后,只需要考虑之前的字符串是否匹配即可。p[j] == '*'
,则dp[i,j] = dp[i-1,j] || dp[i,j-1]
。可以理解为当字符为'*'
的时候会出现两种情况,第一种是'*'
需要作为一个字母与s[i]
进行匹配;第二种是'*'
需要作为空字符(即不需要'*'
可以直接"移除"),所以dp[i,j-1]
;用逻辑或将两种情况连接,是因为只要有一种情况可以匹配成功则当前匹配成功,有点暴力算法的感觉。s[i] !=p [j] && p[j] != '*'
,dp[i,j] = false
。这步可以省略,因为dp
数组元素的默认值就是false
,所以不必要进行显式的赋值为false
。有了上面的理解,我们就可以写代码了。
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软件技术团队会定期开展提升编程技能的刻意练习活动,希望大家能够参与进来一起刻意练习,一起学习进步!
我是 终身学习者“老马”,一个长期践行“结伴式学习”理念的 中年大叔。
我崇尚分享,渴望成长,于2010年创立了“LSGO软件技术团队”,并加入了国内著名的开源组织“Datawhale”,也是“Dre@mtech”、“智能机器人研究中心”和“大数据与哲学社会科学实验室”的一员。
愿我们一起学习,一起进步,相互陪伴,共同成长。
后台回复「搜搜搜」,随机获取电子资源!
欢迎关注,请扫描二维码: