文章目錄
一、題目描述
題目來源:https://leetcode-cn.com/problems/regular-expression-matching
給你一個字符串 s 和一個字符規律 p,請你來實現一個支持 ‘.’ 和 ‘*’ 的正則表達式匹配。
'.' 匹配任意單個字符
'*' 匹配零個或多個前面的那一個元素
所謂匹配,是要涵蓋 整個 字符串 s的,而不是部分字符串。
說明:
s 可能爲空,且只包含從 a-z 的小寫字母。
p 可能爲空,且只包含從 a-z 的小寫字母,以及字符 . 和 *。
示例 1:
輸入:
s = "aa"
p = "a"
輸出: false
解釋: "a" 無法匹配 "aa" 整個字符串。
示例 2:
輸入:
s = "aa"
p = "a*"
輸出: true
解釋: 因爲 '*' 代表可以匹配零個或多個前面的那一個元素, 在這裏前面的元素就是 'a'。因此,字符串 "aa" 可被視爲 'a' 重複了一次。
示例 3:
輸入:
s = "ab"
p = ".*"
輸出: true
解釋: ".*" 表示可匹配零個或多個('*')任意字符('.')。
示例 4:
輸入:
s = "aab"
p = "c*a*b"
輸出: true
解釋: 因爲 '*' 表示零個或多個,這裏 'c' 爲 0 個, 'a' 被重複一次。因此可以匹配字符串 "aab"。
示例 5:
輸入:
s = "mississippi"
p = "mis*is*p*."
輸出: false
二、AC方法
1. 回溯(遞歸解法)
public static boolean isMatch(String s, String p) {
if (p.isEmpty()) {
return s.isEmpty();
}
boolean firstMatch = (!s.isEmpty() && (p.charAt(0) == s.charAt(0) || p.charAt(0) == '.'));
//一種是匹配0個,那麼只需要跳過p中的這兩個字符,繼續與s中的字符進行比較即可,
// 如果是匹配多個(那麼第一個必須也匹配),那麼將s中的遊標往後移動一個,繼續進行判斷,這兩個條件只要其中一個能滿足即可。
if (p.length() > 1 && p.charAt(1) == '*') {
//匹配0個,則只需要跳過p,p移動2位,s保持不變(不需要firstMatch)
//匹配多個,則p保持不變,p的光標還是保持在0位,s移動下一位(同時要求第一位也必須匹配即firstMatch)
return (isMatch(s, p.substring(2)) || firstMatch && isMatch(s.substring(1), p));
} else {
//如果不是"*",則要求當前的第一個字符必須匹配,然後在下一個字符匹配
return firstMatch && isMatch(s.substring(1), p.substring(1));
}
}
2. 自頂向下
參考:https://www.imooc.com/article/281353?block_id=tuijian_wz
2.1 思路過程
假定使用符號:
s[i:] 表示字符串s中從第i個字符到最後一個字符組成的子串
;
p[j:] 表示模式串p中,從第j個字符到最後一個字符組成的子串
;
match(i,j) 表示s[i:]與p[j:]的匹配情況,如果能匹配,則置爲true,否則置爲false。這就是各個子問題的狀態。
那麼對於match(i,j)的值,取決於p[j + 1]是否爲’*’。
curMatch = i < s.length() && s[i] == p[j] || p[j] == ‘.’;
p[j + 1] != ‘*’,match(i,j) = curMatch && match(i + 1, j + 1)
p[j + 1] == ‘*’,match(i,j) = match(i, j + 2) || curMatch && match(i + 1, j)
以s = “aab”; p = "cab"爲例,先構建一個二維狀態空間來存儲中間計算得出的狀態值。橫向的值代表i,縱向的值代表j,match(0,0)的值即問題的解,用f代表false,t代表true
2.2 推算過程
求match(0,0): i = 0; j = 0; curMatch = false;
p[1] == * -> match(0,0) = match(0,2) || false && match(1,0)
轉化爲求子問題match(0,2)和match(1,0)
求match(0,2): i = 0; j = 2; curMatch = true;
p[1] == * -> match(0,2) = match(0,4) || true && match(1,2)
求match(0,4): i = 0; j = 4; curMatch = false;
j + 1 == 5 >= p.length() -> match(0,4) = curMatch = false;
match(0,4) = false;
回溯到第五步,求match(1,2): i = 1; j = 2; curMatch = true;
p[3] == * -> match(1,2) = match(1,4) || true && match(2,2)
求match(1,4): i = 1; j = 4; curMatch = false;
j + 1 == 5 >= p.length() -> match(1,4) = curMatch = false;
match(1,4) = false;
回溯到第10步,求match(2,2): i = 2; j = 2; curMatch = false;
p[3] == * -> match(2,2) = match(2,4) || false && match(3,2)
求match(2,4): i = 2; j = 4; curMatch = true;
j + 1 == 5 >= p.length() -> match(2,4) = curMatch = true;
match(2,4) = true;
回溯到第15步。
match(2,2) = true;
回溯到第10步。
match(1,2) = true;
回溯到第5步。
match(0,2) = true;
回溯到第2步。
match(0,0) = true;
問題解決
2.3 代碼
class Solution {
Result[][] memo;
public boolean isMatch(String text, String pattern) {
memo = new Result[text.length() + 1][pattern.length() + 1];
return dp(0, 0, text, pattern);
}
public boolean dp(int i, int j, String text, String pattern) {
if (memo[i][j] != null) {
return memo[i][j] == Result.TRUE;
}
boolean ans;
if (j == pattern.length()){
ans = i == text.length();
} else{
boolean first_match = (i < text.length() &&
(pattern.charAt(j) == text.charAt(i) ||
pattern.charAt(j) == '.'));
if (j + 1 < pattern.length() && pattern.charAt(j+1) == '*'){
ans = (dp(i, j+2, text, pattern) ||
first_match && dp(i+1, j, text, pattern));
} else {
ans = first_match && dp(i+1, j+1, text, pattern);
}
}
memo[i][j] = ans ? Result.TRUE : Result.FALSE;
return ans;
}
}
3. 自底向上
3.1思路
其實很簡單粗暴,即從最後一個字符開始反向匹配,還是以剛纔的栗子爲例,從i = 3, j = 5 開始依次往左往上循環計算,match(3,5) == true,核心的邏輯並沒有變。因爲最邊緣的值的匹配都是可以直接計算出來的
3.2 代碼
class Solution {
public boolean isMatch(String text, String pattern) {
boolean[][] memo = new boolean[text.length() + 1][pattern.length() + 1];
memo[text.length()][pattern.length()] = true;
for (int i = text.length(); i >= 0; i--){
for (int j = pattern.length() - 1; j >= 0; j--){
boolean curMatch = (i < text.length() &&
(pattern.charAt(j) == text.charAt(i) ||
pattern.charAt(j) == '.'));
if (j + 1 < pattern.length() && pattern.charAt(j+1) == '*'){
memo[i][j] = memo[i][j+2] || curMatch && memo[i+1][j];
} else {
memo[i][j] = curMatch && memo[i+1][j+1];
}
}
}
return memo[0][0];
}
}
4. 動態規劃
4.1 狀態
f[i][j]表示s1的前i個字符,和s2的前j個字符,能否匹配
4.2 轉移方程
如果s1的第 i 個字符和s2的第 j 個字符相同,或者s2的第 j 個字符爲 “.”
f[i][j] = f[i - 1][j - 1]
如果s2的第 j 個字符爲 *
若s2的第 j 個字符匹配 0 次第 j - 1 個字符
f[i][j] = f[i][j - 2] 比如(ab, abc*)
若s2的第 j 個字符匹配至少 1 次第 j - 1 個字符,
f[i][j] = f[i - 1][j] and s1[i] == s2[j - 1] or s[j - 1] == '.'
這裏注意不是 f[i - 1][j - 1], 舉個例子就明白了 (abbb, ab*) f[4][3] = f[3][3]
4.3 初始化
f[0][i] = f[0][i - 2] && s2[i] == *
即s1的前0個字符和s2的前i個字符能否匹配
4.4 結果
f[m][n]
4.5 代碼
public static boolean isMatch1(String s, String p) {
int m = s.length(), n = p.length();
//因爲匹配是從0開始,到最後字符長度m(n)時,所以是m+1(n+1)
boolean[][] f = new boolean[m + 1][n + 1];
f[0][0] = true;
for(int i = 2; i <= n; i++){
//s1的前0個字符和s2的前i個字符能否匹配(只有當p的前一個字符爲*時才匹配,*可以匹配前面的0個)
f[0][i] = f[0][i - 2] && p.charAt(i-1) == '*';
}
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
if(s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.'){
f[i][j] = f[i - 1][j - 1];
}
if(p.charAt(j - 1) == '*'){
f[i][j] = f[i][j - 2] ||
f[i - 1][j] && (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.');
}
}
}
return f[m][n];
}