LeetCode 熱題 HOT 100(05,正則表達式匹配)

LeetCode 熱題 HOT 100(05,正則表達式匹配)

不夠優秀,髮量尚多,千錘百煉,方可成佛。

算法的重要性不言而喻,無論你是研究者,還是最近比較火熱的IT 打工人,都理應需要一定的算法能力,這也是面試的必備環節,算法功底的展示往往能讓面試官眼前一亮,這也是在大多數競爭者中脫穎而出的重要影響因素。

然而往往大多數人比較注重自身的實操能力,着重於對功能的實現,卻忽視了對算法能力的提高。有的時候採用不同的算法來解決同一個問題,運行效率相差還是挺大的,畢竟我們最終還是需要站在客戶的角度思考問題嘛,能給用戶帶來更加極致的體驗當然再好不過了。

萬法皆空,因果不空。Taoye之前也不怎麼情願花費太多的時間放在算法上,算法功底也是相當的薄弱。這不,進入到了一個新的學習階段,面對導師的各種“嚴刑拷打”和與身邊人的對比,纔開始意識到自己“菜”的事實。

這次的題目是LeeTCode 熱題 HOT 100的第六題,難度屬於困難,主要考查的是正則匹配問題。

感覺這道題還是有點東西的,也是花費了不少時間才搞懂。

我們都知道正則表達式主要用來進行字符匹配的,在爬蟲中,我們會在向服務器發出一個請求並得到相應結果之後,通過特定的方式在響應結果中提取我們所需要的目標數據,而其中一種方式就可以通過正則表表達式來進行解析。

這道題的難度還是有點的,讓我更加體會到了算法“孰能生巧”這一特性,這就像刷數學題一樣,題目見過就有思路,沒見過根本完全無法動手。這道題也是一樣,主要可以通過動態規劃算法來進行求解,當然也有其他可供用的算法,本文主要使用的是動態規劃算法。

下面,我們就來看看這道題吧。

題目:正則表達式匹配

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

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

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/regular-expression-matching

示例

  • 示例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

思路

前面也提到了,爬蟲中會經常會使用到正則表達式,既然如此,肯定是有特定的模塊可供調用的,而我們可以通過re模塊來實現這一功能:

class Solution(object):
    def isMatch(self, s, p):
        return True if re.match(p+'$',s) else False

需求雖已實現,但顯然我們需要手動來實現這一功能,而非調用第三方模塊。

在正式通過動態規劃解決該題之前,我們有必要先了解下動態規劃,其能解決哪些問題。

  • 計數問題,比如在一個矩陣當中,有多少種方式能從左上角走到右下角。(每次行走方向只能往下或往右)
  • 求最值問題,比如有三種硬幣,面值分別是2元、3元、7元,則27元最少能用多少枚硬幣組成
  • 存在性問題,比如說我們的這道題,是否能夠在目標字符串中匹配成功

當然了,我們肯定是不能一概而論的。具體問題,具體分析,還是要根據實際情況來判斷目標問題是否能夠通過動態規劃來解決。

要在問題中使用動態規劃,我們一般需要四個步驟:

  1. 確定狀態(最後一步、化爲子問題)。解動態規劃時需要定義一個數組,而確定狀態就是要明白數組的每個元素dp[i]或者dp[i][j]代表什麼意思(讀到這裏,可能會有點糊塗,沒事,下面會有示例來具體解釋)
  2. 轉移方程,就是通過上一步驟當中的子問題來得到目標問題的子問題
  3. 初始條件和邊界情況,動態規劃一般是通過前一個節點來獲取下一節點的值,所以我們需要明確其初始條件和數組dp的邊界條件。就像數學歸納法,或是數列當中遞推公式。
  4. 計算順序,就是明確實現需求之前的上一個步驟要獲得什麼

讀到這裏,可能會有點糊塗,沒事,下面會用示例來具體解釋:

示例1:有三種硬幣,面值分別是2元、3元、7元,則27元最少能用多少枚硬幣組成

確定狀態(最後一步、化爲子問題):我們知道,題目需要的是最少能用多少枚硬幣組成,我們需要定義一個長度爲27的數組dp,而dp[i]就代表了對應總額所需要的最少硬幣數目。我們假設最後一枚硬幣面值爲,且最少需要枚硬幣組成,則除去最後一枚硬幣的總值爲,且前面枚硬幣的枚數所組成的面值也應該是最少的。所以,我們把原問題就轉化爲了子問題:最少可以通過多少枚硬幣組成元(枚)

轉移方程:通過如上分析,我們可以得到如下遞推公式(轉移方程),代表的意思就是除去最後一枚硬幣的面值總額所需要的最少硬幣數 + 1

初始條件和邊界情況:初始條件就是f(0),表示的就是0元可以通過0枚硬幣組成,而邊界情況就是每當面值總額減2、5、7的時候都應該大於0,否則的話無法組成目標面額總值(根據題意自行理解)

計算順序:要想獲得f(x),就必須得到f(x-2)、f(x-5)、f(x-7)的值,也就是除去一枚硬幣面額所需要的最少硬幣數目

通過如上分析,可以得到如下Java算法;

public int coinChange(int[] coin_list, int coin_value) {
    int[] f = new int[coin_value + 1];
    f[0] = 0;
    for (int i = 1; i <= coin_value; i++) {
        f[i] = Integer.MAX_VALUE;
        for (int j = 0; j < coin_list.length; j++) {
            if (i >= coin_list[j] && f[i - coin_list[j]] != Integer.MAX_VALUE) {
                f[i] = Math.min(f[i], f[i - coin_list[j]] + 1);
            }
        }
    }
    if (f[coin_value] == Integer.MAX_VALUE) { f[coin_value] = -1; }
    return f[coin_value];
}

示例2:在一個矩陣當中,有多少種方式能從左上角走到右下角。(每次行走方向只能往下或往右),比如從(0, 0)出發,有多少種路線走到(4, 5)

時間關係,這裏簡單分析下。

走到(4, 5)的上一格有兩種,一個是(3, 5),另一個是(4, 4),所以走到(4, 5)的路線總數就等於上述兩種的總和,對此,我們可以得到如下關係,其中f[i][j]表示的是走到(i, j)的路線種數

Python實現的代碼如下:

def calc_run_number(m, n):
    import numpy as np
    f = np.zeros([m, n])
    for i in range(m):
        for j in range(n):
            if (i ==0 or j == 0): f[i][j] = 1
            else: f[i, j] = f[i-1][j] + f[i][j-1]
    return f[m-1][n-1

像類似的案例其實還有很多的,主要還是要多多聯繫纔行。

現在,我們迴歸到正則匹配這道題,再次看下題面:

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

  • 匹配零個或多個前面的那一個元素
    所謂匹配,是要涵蓋 整個 字符串 s的,而不是部分字符串。

前面也有說到,使用動態規劃之前需要定義一個數組來存儲數據狀態,這道題中涉及s和p兩個字符串,所以我們需要定義二維數組,也就是矩陣dp。假設s和p的長度分別爲s_len和p_len,則dp數組的shape值應該爲(s_len + 1, p_len + 1),其中+1主要是的因爲需要額外引入一行一列,用於表示空字符""的匹配情況,這裏可以理解成初始值的引入。

dp[i][j]表示的就是s[:i]能否通過dp[:j]正則表達式來進行匹配,其值爲True或False。True表示的是能匹配成功,比如s:abc p:a.c,而False表示的是不能匹配,比如s:abc p:ab*,這一點一定要理解清楚,非常重要,也是解這道題的關鍵之一。

我們需要對dp數組進行初始化,不如通過如下方式將內部所有元素初始化爲False:,且dp[0][0]=True,因爲空字符s自然能用空字符p來進行匹配:

dp = [[False] * (p_len + 1) for temp in range(s_len + 1)]; dp[0][0] = True

隨後,需要將dp第一行進行額外處理,也就是初始狀態的定義。這裏的有個條件來對*進行判斷,假如說*前面的一個字符存在,則需要將該位置上的dp值定義爲與前兩個值相同,因爲在正則表達式中,*表示的是能匹配零個或多個前面的那一個元素(注意:這是在第一行進行定義,也就是說此時s可以看做""空字符,通過p[:j]正則來對其進行匹配):

for one_row_item in range(p_len):
    if p[one_row_item] == '*' and dp[0][one_row_item - 1]:
        dp[0][one_row_item + 1] = True

第一行處理完成,隨後就需要對其他行的dp值進行填充,而填充的依據就是根據上一行來進行分類判斷:假設遍歷條件滿足:p[j - 1] == s[i - 1] or p[j - 1] == '.',也就是說s和p對應的字符相同,或能通過.匹配任意字符,則將設置dp[i][j] = dp[i - 1][j - 1],因爲此時dp[i][j]的匹配情況是由dp[i-1][j-1]來決定的;假如說此時的正則字符爲*,則dp[i][j]需要根據dp[i][j - 2]、dp[i][j - 1]、dp[i - 1][j]共同決定,完整算法思想如下:

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        s_len, p_len = len(s), len(p)
        dp = [[False] * (p_len + 1) for temp in range(s_len + 1)]; dp[0][0] = True
        for one_row_item in range(p_len):
            if p[one_row_item] == '*' and dp[0][one_row_item - 1]:
                dp[0][one_row_item + 1] = True
        for i in range(1, s_len + 1):
            for j in range(1, p_len + 1):
                if p[j - 1] == s[i - 1] or p[j - 1] == '.': dp[i][j] = dp[i - 1][j - 1]
                elif p[j - 1] == '*':
                    if p[j - 2] != s[i - 1]: dp[i][j] = dp[i][j - 2]
                    if p[j - 2] == s[i - 1] or p[j - 2] == '.':
                        dp[i][j] = dp[i][j - 2] or dp[i][j - 1] or dp[i - 1][j]
        return dp[s_len][p_len]

總的來說,這道題還是有難度的,至少是目前刷到最難的一題了,也是花費了不少時間來理解,理解了也很難通過文字來進行描述,主要還是要通過閱讀算法代碼來理解其核心思想。

像這種通過動態規劃算法來解決的問題還是要多做多練,才能孰能生巧。

我是Taoye,愛專研,愛分享,熱衷於各種技術,學習之餘喜歡下象棋、聽音樂、聊動漫,希望藉此一畝三分地記錄自己的成長過程以及生活點滴,也希望能結實更多志同道合的圈內朋友,更多內容歡迎來訪微信公主號:玩世不恭的Coder

推薦閱讀:

LeetCode 熱題 HOT 100(00,兩數之和)
LeetCode 熱題 HOT 100(01,兩數相加)
LeetCode 熱題 HOT 100(02,無重複字符的最長子串)
LeetCode 熱題 HOT 100(03,尋找兩個正序數組的中位數)
LeetCode 熱題 HOT 100(04,最長迴文子串))

參考資料:

[1] LeetCode官方:https://leetcode-cn.com/problems/regular-expression-matching/solution/zheng-ze-biao-da-shi-pi-pei-by-leetcode-solution/
[2] 動態規劃算法:https://www.bilibili.com/video/BV1xb411e7ww?from=search&seid=9800329040911246241

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章