字符串匹配问题算法总结

@author stormma
@date 2018/03/24


生命不息,奋斗不止


题目1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Implement wildcard pattern matching with support for '?' and '*'.
'?' Matches any single character.
'*' Matches any sequence of characters (including the empty sequence).
The matching should cover the entire input string (not partial).
The function prototype should be:
bool isMatch(const char *s, const char *p)
Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "*") → true
isMatch("aa", "a*") → true
isMatch("ab", "?*") → true
isMatch("aab", "c*a*b") → false

DP思路分析

首先我们选取状态, 用boolean[][] dp数组来标记s中i个字符串是否匹配于p中j个字符串, 当然如果匹配则为True, else False
下面我们来分析一下状态是怎么变化的。

且看图:
字符串匹配问题分析图

通过上面的状态转移图, 我们很容易可以得到:

  1. 边界初始化, 当si = 0;(即s字符串取0个字符), ps 0 -> p.length()的初始化为: 如果此时的p[pi]这个字符是*的话,
    那么它的值应该是上一个的值, 即dp[0][pi] = dp[0][pi - 1], 如果此时p[pi] != '*'呢, 那么肯定是False. 当pi = 0;,
    初始化dp[si][0] = False && si >= 1(p中取出0个字符, s中取出任意个(>0), 都是不匹配的), dp[0][0] = True(此时s中取0个字符和p中取0个字符, 无疑是匹配的).
  2. 边界初始化完成之后, 我们分析一下其他地方怎么转移. 分为以下两种情况来讨论:
    • if p[pi] == '*', 无非是从它的左边和上边转移而来的。
    • if p[pi] == '?' || p[pi] == s[si], 那么它肯定是从它左上角转移而来。

状态转移分析完成之后, 显然答案便是dp[s.length()][p.length()]

AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* dp解决
* time running time: O(n*m)
* extra space: O(n*m)
*/
static class Solution2 {
public boolean isMatch(String s, String p) {
if (s == null && p == null) return true;
if (s == null || p == null) return false;
// dp[i][j]表示s字符串中前i个和p中前j个字符是否匹配
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;
// 初始化边界
for (int i = 1; i <= p.length(); i++) {
dp[0][i] = p.charAt(i - 1) == '*' && dp[0][i - 1];
}
for (int i = 1; i <= s.length(); i++) {
for (int j = 1; j <= p.length(); j++) {
if (p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '*') {
// ab abcd
// ab* ab*
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
}
}
}
return dp[s.length()][p.length()];
}
}

解法2

我们可以用一个指针来记录*出现的位置, 再用两个指针标记sp的位置: sppp指针, 再定义一个
辅助变量match, 分析如下:

  1. 如果此时s[sp] == p[pp] || p[pp] == '?', 那么我们只需让sppp指针走一步即可。
  2. 如果此时的p[pp] == '*', 我们用star来记录一下此时*出现的位置, 用辅助变量match来记录一下
    此时的s走到了哪个地方, 然后让pp再走一步。
  3. 如果此时star存在有效的值, 并且都不满足以上两种情况, 那么我们让pp回到上一次出现*的下一个位置, 让p
    回到上次出现*的时候, s的位置的下一个位置。

为什么这样做呢? 我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s = bbaec
p = *c
sp = 0, pp = 0, match = 0, star = pp = 0, match = sp = 0 => pp++ => pp = 1
sp = 0, pp = 1, match = 0, star = 0; 此时b != c :
然后s继续走: pp = star + 1 = 1, match++ => match = 1, sp = 1
sp = 1, pp = 1, match = 1, star = 0, 此时 b != c:
s继续走: pp = star + 1 = 1, match++ => match = 2, sp = 2
sp = 2, pp = 1, match = 2, star = 0, 此时 a != c:
s继续走: pp = star + 1 = 1, match++ => match = 3, sp = 3
sp = 3, pp = 1, match = 3, star = 0, 此时 e != c:
s继续走: pp = star + 1 = 1, match++ => match = 4, sp = 4
sp = 4, pp = 1, match = 4, star = 0, 此时 c == c:
sp++ => sp = 5, pp++ => pp = 2
退出循环, 说明`s`走到了头, 但是不确定`p`是否走到了头, 外加一个判断p的循环即可解决。

说到底, 如果遇到不符合字符串不相等并且不存在*的时候, 就回退到上一个star出现的地方, 然后跳过可能被*
匹配的字符。

AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static class Solution1 {
/**
* time running: O(n)
* bbarc
* *c
* @param s
* @param p
* @return
*/
public boolean isMatch(String s, String p) {
if (s == null && p == null) return true;
if (s == null || p == null) return false;
int sp = 0, pp = 0, star = -1, match = 0;
while (sp < s.length()) {
if (pp < p.length() && (s.charAt(sp) == p.charAt(pp) || p.charAt(pp) == '?')) {
sp++;
pp++;
} else if (pp < p.length() && p.charAt(pp) == '*') {
star = pp;
match = sp;
pp++;
} else if (star != -1) {
pp = star + 1;
match++;
sp = match;
} else {
return false;
}
}
while (pp < p.length() && p.charAt(pp) == '*') pp++;
return pp == p.length();
}
}

题目2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Implement regular expression matching with support for '.' and '*'.
'.' Matches any single character.
'*' Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
The function prototype should be:
bool isMatch(const char *s, const char *p)
Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true

DP思路分析

此题和上一个题很类似, 我们还是采用dp来解决, 状态选择情况一样, boolean [][] dp = new boolean[s.length() + 1][p.length() + 1]
状态转移也很简单, 在这就不画图分析了, 直接给出AC代码。

AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static class Solution {
public boolean isMatch(String s, String p) {
if (p.isEmpty()) {
return s.isEmpty();
}
boolean[][] dp = new boolean[s.length() + 1][p.length() + 1];
dp[0][0] = true;
for (int j = 1; j <= p.length(); j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2];
}
}
for (int i = 1; i <= s.length(); i++) {
for (int j = 1; j <= p.length(); j++) {
if (p.charAt(j - 1) == '.' || s.charAt(i - 1) == p.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '*') {
if (p.charAt(j - 2) == s.charAt(i - 1) || p.charAt(j - 2) == '.') {
dp[i][j] = dp[i][j - 2] || dp[i - 1][j];
} else {
dp[i][j] = dp[i][j - 2];
}
}
}
}
return dp[s.length()][p.length()];
}
}