LeetCode 不只是題解(10.正規表示式匹配[困難])

Bug_ancestor發表於2020-10-06

題目

附上題目連線

給你一個字串 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 = “cab”
輸出: true
解釋: 因為 ‘*’ 表示零個或多個,這裡 ‘c’ 為 0 個, ‘a’ 被重複一次。因此可以匹配字串 “aab”。
示例 5:

輸入:
s = “mississippi”
p = “misisp*.”
輸出: false

題目解析

p : a * b c *

為了描述方便,我們將上述的a * 、b、c *分別成為一次匹配的要求。

從題目中可以提取中一些關鍵的地方:

  • 匹配的是整個字串。兩個字串要相互匹配才可以,任何一個有多餘都不可以;

  • 注意. *是可以匹配任意字元任意次。一個. *可以匹配任意的字串;

  • 注意*對匹配的影響:

    s : aaaaabc q: a*aaabc

    很明顯這個例子應該是true。從上面例子應該能發現一些不對勁的地方,就是*的匹配次數不僅取決於本次匹配的要求,而且還取決於下次匹配的要求。

題目解析及優化

  1. 遞迴法

    我們首先要求和把每次提取要求的函式定義到外部方便使用,其他地方都好說,關鍵是出現字元+ * 的形式部分的寫法。

    我們的思路也很簡單。* 這小子不是匹配的次數不一定嘛,好說,我們每次都試試,如果有一次是正確的,本次就是可以匹配的。出現*,就往後遞迴,把可能匹配的情況都寫出來去尋找。

    具體的實現思路:

    其實這部分無非兩種情況:

    s : abcd p : af*bcd

    s : abbbc p : ab*c

    • 當出現 * 的匹配要求的字元和s部分的不匹配(b和f *):此時直接忽略f * 直接向後繼續匹配即可
    • 當出現 * 的匹配要求的字元和s部分的匹配(bbb和b *): 此時只需要兩個遞迴 :一個忽略b *向後繼續匹配;另一個將一個b和b * 匹配,繼續向後匹配。

    貼上程式碼:

    //匹配的要求
    struct request {
        char c;
        int leixing;
    };
    
    //型別為0為*,1為匹配一次
    request find_one_requ(string q, int index) {
        request ans;
        ans.c = q[index];
        if (index < q.size() - 1 && q[index + 1] == '*') {
            ans.leixing = 0;
        } else {
            ans.leixing = 1;
        }
        return ans;
    }
    
    bool isMatch(string s, string p) {
        int i = 0, j = 0;
        //以p為主進行迭代
        for (; i < p.size(); i++) {
            request re = find_one_requ(p, i);
            //如果s匹配完時退出
            if (re.leixing && j == s.size()) {
                return false;
            }
            //如果只需匹配一次時
            if (re.leixing) {
                if (re.c == '.' || re.c == s[j]) {
                    j++;
                } else if (re.c != s[j]) {
                    return false;
                }
            //出現*的時候
            } else {
                //需要出現兩次迭代的情況
                if ((re.c == '.' || re.c == s[j]) && j < s.size()) {
                    return isMatch(s.substr(j, s.size() - j), p.substr(i + 2, p.size() - i - 2)) ||
                           isMatch(s.substr(j + 1, s.size() - j - 1), p.substr(i, p.size() - i));
                //只需迭代一次的情況
                } else {
                    return isMatch(s.substr(j, s.size() - j), p.substr(i + 2, p.size() - i - 2));
                }
                i++;
            }
        }
    	//當p迭代完時根據情況返回
        if (j == s.size()) {
            return 
                true;
        } else {
            return false;
        }
    }
    
  2. 動態規劃

    再分析一波上面解法的思路

    s : bbbc p : b * b * c

    看下圖即可:

在這裡插入圖片描述

一旦有兩個以上的*號就很有可能會出現重複的計算。而且大量使用遞迴會導致佔用更大的記憶體。避免重複計算的方法正是動態規劃。

每一次匹配都是基於之前的匹配結果的,所以可以自然的想到動態規劃的開始是兩個空串可以相互匹配,所以搭建dp [ i ] [ j ]的二維陣列而且dp [ 0 ] [ 0 ]為true;

狀態轉移方程我們可以參考遞迴方法的遞迴部分,思路一致,只是寫法有所區別:

  • 首先並不需要匹配要求的結構體而直接一個字元一個字元往後匹配即可。這是因為當dp[ i ] [ j ]匹配到 * 的時候,* 前面的字元已經進行了一次匹配,我們視作dp[ i ] [ j - 1 ]為匹配一次的情況即可。並不影響我們的判斷。
  • 分情況依舊按照匹配要求的型別去分,只是這裡只需要判斷匹配的是不是 * 即可判斷匹配的型別。

在這裡插入圖片描述

這裡就直接貼官方的程式碼:

class Solution {
public:
    bool isMatch(string s, string p) {
        int m = s.size();
        int n = p.size();

        auto matches = [&](int i, int j) {
            if (i == 0) {
                return false;
            }
            if (p[j - 1] == '.') {
                return true;
            }
            return s[i - 1] == p[j - 1];
        };

        vector <vector<int>> f(m + 1, vector<int>(n + 1));
        f[0][0] = true;
        for (int i = 0; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p[j - 1] == '*') {
                    f[i][j] |= f[i][j - 2];
                    if (matches(i, j - 1)) {
                        f[i][j] |= f[i - 1][j];
                    }
                } else {
                    if (matches(i, j)) {
                        f[i][j] |= f[i - 1][j - 1];
                    }
                }
            }
        }
        return f[m][n];
    }
};

總結及心得

  • 本題的破題點就是當不知道 * 匹配幾次的時候全部匹配一次,有一次成功即可,這就形成了遞迴法的思路;動態規劃則需要對整個過程比較熟悉才能寫出,我個人的建議是先思考遞迴,再用動態規劃去模擬遞迴的思路去降低時間和空間複雜度;

  • 在寫遞迴式的時候一定要考慮到邊界問題,有時遞迴的隱含條件和邊界有關,如果忽略可能會導致陣列越界;

  • 一種特殊函式的寫法

    auto matches = [&](int i, int j) {
                if (i == 0) {
                    return false;
                }
                if (p[j - 1] == '.') {
                    return true;
                }
                return s[i - 1] == p[j - 1];
            };
    

    這樣可以直接在函式內部定義,可以避免傳引數的問題,也可以避免不想傳參就定義全域性變數的問題;

相關文章