【leetcode】如何實現 regex 正則表達式引擎

image

題目

給你一個字符串 s 和一個字符規律 p,請你來實現一個支持 '.' 和 '*' 的正則表達式匹配。java

'.' 匹配任意單個字符 '*' 匹配零個或多個前面的那一個元素 所謂匹配,是要涵蓋 整個 字符串 s的,而不是部分字符串。正則表達式

說明:less

s 可能爲空,且只包含從 a-z 的小寫字母。函數

p 可能爲空,且只包含從 a-z 的小寫字母,以及字符 . 和 *。性能

我的分析

拿到題目的第一反應就是這是一個 regex 表達式解析引擎,可是過於複雜。spa

因而能夠按照必定的順序去實現。code

下面來逐步看一下這個題目的解答過程。blog

v1 標準庫實現版本

代碼

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

性能

Runtime: 64 ms, faster than 24.57% of Java online submissions for Regular Expression Matching.
Memory Usage: 40.3 MB, less than 7.95% of Java online submissions for Regular Expression Matching.

雖然實現了,可是對於咱們我的基本沒有任何收益。遞歸

也沒有體會到 regex 解析過程的快樂,並且性能也不怎麼樣。rem

v2 遞歸實現

實現思路

若是 p 中沒有任何 * 號,那麼對比起來其實比較簡單,就是文本 s 和 p 一一對應。

. 對應任意單個字符,也不難。

若是存在 * 號

若是存在 *,這個問題就會難那麼一些。

public boolean isMatch(String s, String p) {
    // 若是 p 已經遍歷結束,直接看 s 是否結束。
    if(p.isEmpty()) {
        return s.isEmpty();
    }
​
    // 第一位是否匹配判斷
    boolean firstMatch = !s.isEmpty() && (s.charAt(0) == p.charAt(0) || p.charAt(0) == '.');
​
    if(p.length() >= 2 && p.charAt(1) == '*') {
        // 1. 第一位匹配 && 後續匹配 (* 一次或者屢次)
        // 2. c* 出現零次,則直接全文本匹配。
        return (firstMatch && isMatch(s.substring(1), p)) || isMatch(s, p.substring(2));
    } else {
        // 第二位不是 *,則直接跳過第一位看後續的信息。
        return firstMatch && isMatch(s.substring(1), p.substring(1));
    }
}

效果

這個用到了遞歸,性能以下:

Runtime: 88 ms, faster than 8.51% of Java online submissions for Regular Expression Matching.
Memory Usage: 39.8 MB, less than 27.13% of Java online submissions for Regular Expression Matching.

一個字,慘~

v3 動態規劃

對於遞歸的思考

你也許發現了,原來的代碼中

isMatch(s.substring(1), p.substring(1))

這種相似的匹配結果,其實是一次次的在重複的。

好比第一次咱們匹配 [1, 10],後續又匹配 [2, 10]

這樣若是你學過 DP 那麼會有一個想法,可否重複利用已經判斷過的內容呢?

DP 無敵。

解題思路

咱們用遞歸中一樣的回溯方法,除此以外,由於函數 match(text[i:], pattern[j:]) 只會被調用一次,咱們用 dp(i, j) 來應對剩餘相同參數的函數調用,這幫助咱們節省了字符串創建操做所須要的時間,也讓咱們能夠將中間結果進行保存。

自頂向下的方法

這裏的核心區別就是不對 text/pattern 作 substring 的操做,而是從前日後處理。

enum Result { 
    TRUE, FALSE
}
​
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()){
            // 若是 pattern 已經遍歷結束
            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;
    }
}

效果

Runtime: 3 ms, faster than 83.97% of Java online submissions for Regular Expression Matching.
Memory Usage: 39.9 MB, less than 22.69% of Java online submissions for Regular Expression Matching.

性能仍是不錯的。

實際上這個性能是比實現一個 regex 引擎要好的,由於 regex 的編譯構建 DFA/NFA 很是的耗時。

自上而下

理解了上面的解法,下面的解法就比較簡單了。

從後往前處理,這樣就避免掉了默認值的問題,不須要像上面同樣引入一個奇奇怪怪的枚舉值。

public boolean isMatch(String s, String p) {
    //dp 存放的是後面處理的結果
   boolean[][] dp = new boolean[s.length()+1][p.length()+1];
   dp[s.length()][p.length()] = true;
​
   for(int i = s.length(); i >= 0; i--) {
        for(int j = p.length()-1; j >= 0; j--) {
            // 核心代碼保持不變
            // 這裏不用判斷是否爲 empty 的問題
            boolean firstMatch = i < s.length() && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '.');
            // 判斷星號
            if(j+1 < p.length() && p.charAt(j+1) == '*') {
                // 出現零次
                // 一次或者屢次
                dp[i][j] = dp[i][j+2] || (firstMatch && dp[i+1][j]);
            } else {
                dp[i][j] = firstMatch && dp[i+1][j+1];
            }
        }
   }
​
   // 直接返回結果
   return dp[0][0];
}

效果

還算比較優雅,性能還算滿意。

Runtime: 2 ms, faster than 92.84% of Java online submissions for Regular Expression Matching.
Memory Usage: 38.3 MB, less than 73.31% of Java online submissions for Regular Expression Matching.

小結

雖然咱們使用過不少次 Regex 正則表達式,可是實際上實現起來可能沒有使用那麼簡單。

後續有機會咱們能夠講述寫如何實現一個相對完整的正則表達式引擎。

image

相關文章
相關標籤/搜索