演算法之字串——正規表示式匹配

it_was發表於2020-09-29

劍指 Offer 19. 正規表示式匹配

難度 困難:worried:

請實現一個函式用來匹配包含’. ‘和’*‘的正規表示式。模式中的字元’.’表示任意一個字元,而’*‘表示它前面的字元可以出現任意次(含0次)。在本題中,匹配是指字串的所有字元匹配整個模式。例如,字串”aaa”與模式”a.a”和”abaca”匹配,但與”aa.a”和”ab*a”均不匹配。

  • 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

一開始的思路,,,,好吧沒有思路:sob: 只能使用字串中關於正則的匹配API,程式碼如下

class Solution {
    public boolean isMatch(String s, String p) {
        return s.matches(p);
    }
}

其實我們仔細考慮一下,無非就是分三種情況:raising_hand:

假設主串為 A,模式串為 B , A 的長度為 n ,B 的長度為 m ,關注正規表示式 B 的最後一個字元是誰,它有三種可能,正常字元,* 和 .(點),那針對這三種情況討論即可,如下:

  • 如果 B 的最後一個字元是正常字元,那就是看 A[n-1] 是否等於 B[m-1],相等則看 A_{0..n-2}與 B_{0..m-2},不等則是不能匹配,這就是子問題。:clock1:

  • 如果 B 的最後一個字元是 .(點) ,它能匹配任意字元,直接看 A_{0..n-2}與 B_{0..m-2}:clock4:

  • 如果 B 的最後一個字元是 * ,它代表 B[m-2]=c 可以重複0次或多次,它們是一個整體 c*:clock7:

    • 情況一:A[n-1]是 0個 c,B 最後兩個字元廢了,能否匹配取決A_{0..n-1}和 B_{0..m-3}是否匹配
    • 情況二:A[n-1]是多個 c 中的最後一個(這種情況必須 A[n−1]=c 或者 c=’.’),所以 A 匹配完往前挪一個,B 繼續匹配,因為可以匹配多個,繼續看 A_{0..n-2}和 B_{0..m-1}是否匹配。

先來個某大神動態規劃的思路:boom:

轉移方程
f[i][j] 代表 A 的前 i 個和 B 的前 j 個能否匹配
對於前面兩個情況,可以合併成一種情況 f[i][j] = f[i-1][j-1]
對於第三種情況,對於 c* 分為看和不看兩種情況

  • 不看:直接砍掉正則串的後面兩個, f[i][j] = f[i][j-2]
  • 看:正則串不動,主串前移一個,f[i][j] = f[i-1][j]

特判:需要考慮空串空正則

  • 空串和空正則是匹配的,f[0][0] = true
  • 空串和非空正則,不能直接定義 true 和 false ,必須要計算出來。(比如A=” “ ,B=abc*)
  • 非空串和空正則必不匹配,f[1][0]=…=f[n][0]=false
  • 非空串和非空正則,那肯定是需要計算的了。

大體上可以分為空正則和非空正則兩種,空正則也是比較好處理的,對非空正則我們肯定需要計算,非空正則的三種情況,前面兩種可以合併到一起討論,第三種情況是單獨一種,那麼也就是分為當前位置是 * 和不是 * 兩種情況了。

再來個遞迴程式碼,遞迴程式碼與上面的思路其實是一樣的

package ddx.september.day29;

public class Normal_19 {
    public boolean isMatch(String s, String p) {
        if (s.length() == 0 ) {
            //主串為空
            if (p.length() % 2 != 0) {
                //模式串長度為奇數,則一定不匹配,因為抵消不了奇數
                //例如:"." , "ab*","abc"
                return false;
            } else {
                //長度為偶數,只需要判斷偶數位上是不是*即可!
                int i = 1;
                while (i < p.length()) {
                    if (p.charAt(i) != '*') {
                        return false;
                    }
                    i += 2;
                }
                return true;
            }
        } else if (p.length() == 0) {
            //模式串為空
            return false;
        } else {
            char ch1 = s.charAt(0); //s的首位
            char ch2 = p.charAt(0); //p的首位
            char ch3 = 'a';         //p的下一位
            if(p.length() > 1){
                ch3 = p.charAt(1);
            }
            //照樣分情況,看看是不是*
            if(ch3 != '*'){
                if(ch2 == ch1 || ch2 == '.'){
                    return  isMatch(s.substring(1),p.substring(1));
                }else{
                    //連正常字元和.都匹配不上
                    return false;
                }
            }else{
                //難點就在於ch3是 *
                //正常字元相等,或者是.
                //即選擇或者不選擇!!!
                //"aa"
                //"a*a"
                //上面兩個字串,碰到*可以選擇,也可以不選擇
                if(ch2 == ch1 || ch2 == '.'){
                    //前者是選擇,後者是不選擇
                    return isMatch(s.substring(1),p) || isMatch(s,p.substring(2));
                }else{
                    //連正常字元和.都匹配不上,那麼ch2 和 ch3廢了!!!!
                    return isMatch(s,p.substring(2));
                }
            }
        }
    }
}

演算法之字串——正規表示式匹配(二)

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章