徒手挖地球十七週目
NO.44 通配符匹配 困難
這道題和NO.10正則表達式匹配看起來很像,正則匹配的題解可以參考徒手挖地球十六週目中的記錄。
這兩道題目的區別在於’*‘的處理不同,正則中的星號是星號前的字符可以出現0次、1次或多次,而本題中通配符中的星號則是可以匹配任意字符。但是正則中的’.‘和通配符中的’?'作用是一樣的。
所以說這道題的難點一樣是對於’*'的處理。
思路一:雙指針貪心算法 重點就是我們如何充分的利用’*’。
我們用i和j分別標記s和p的第一個字符下標,即都初始化爲0。用istart和jstart分別標記s和p中’*'匹配過的位置,即初始化爲-1。
和普通字符串匹配的思路差不多,已經匹配成功的部分就不再考慮了
,所以要用i和j標記當前正在比較的字符;但是最近匹配過的'*'
可能會被重複使用去匹配更多的字符
,所以我們要用istart和jstart分別標記s和p中最近匹配過'*'的位置
。可以參考徒手挖地球十六週目NO.正則表達式匹配的思路一是如何從普通情況延伸到特殊字符的。
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 boolean isMatch(String s, String p) {
if (p==null||p.isEmpty())return s==null||s.isEmpty();
int i=0,j=0,istart=-1,jstart=-1,slen=s.length(),plen=p.length();
//判斷s的所有字符是否匹配
while (i<slen){
//三種匹配成功情況以及匹配失敗返回false
if (j<plen&&(s.charAt(i)==p.charAt(j)||p.charAt(j)=='?')){
i++;
j++;
}else if (j<plen&&p.charAt(j)=='*'){
istart=i;
jstart=j++;
}else if (istart>-1){
i=++istart;
j=jstart+1;
}else {
return false;
}
}
//s中的字符都判斷完畢,則認爲s爲空,此時需要p爲空或者p中只剩下星號的時候,才能成功匹配。
//如果p中剩餘的都是*,則可以移除剩餘的*
while (j<plen&&p.charAt(j)=='*')j++;
return j==plen;
}
時間複雜度:O(mn) m、n分別是s和p的長度。
思路二:動態規劃法 分析步驟依然按照徒手挖地球十六週目NO.正則表達式匹配思路二的步驟來。
dp數組的含義:dp[i][j]意思是s的前i個元素能否被p的前j個元素成功匹配。
知道了dp數組的含義之後,我們就知道了初始化細節:
boolean類型
的dp數組,大小是[s.length+1][p.length+1]
,因爲存在s前0個字符和p前0個字符的情況。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。
public boolean isMatch(String s, String p) {
if (p==null||p.isEmpty())return s==null||s.isEmpty();
int slen = s.length(),plen=p.length();
boolean[][] dp=new boolean[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.charAt(j-1)=='*'&&dp[0][j-1];
//填寫dp數組剩餘部分
for (int i = 1; i <= slen; i++) {
for (int j = 1; j <= plen; j++) {
char si = s.charAt(i - 1),pj=p.charAt(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];
}
時間複雜度:O(mn) m、n分別是s和p的長度。
本人菜鳥,有錯誤請告知,感激不盡!