我最近複習一道困難程度的算法題,發現了許多有趣之處。在借鑑了他人解法後,發現從最簡單的狀況反推到原題是一種解鎖新進階的感受。從遞歸到動態規劃,思惟上一步一步遞進,如同一部跌宕起伏的小說,記錄下來和諸君共賞之。正則表達式
題目以下:算法
給你一個字符串 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 = "c*a*b" 輸出: true 解釋: 由於 '*' 表示零個或多個,這裏 'c' 爲 0 個, 'a' 被重複一次。所以能夠匹配字符串 "aab"。 示例 5: 輸入: s = "mississippi" p = "mis*is*p*." 輸出: false 來源:力扣(LeetCode)
這是一道關於字符串匹配的問題,其中匹配字符串裏面可能含有兩種特殊符號「.」和「*」。函數
說時候剛拿到這道題的時候我很懵逼,直接動手分析到帶有「*」符號的時候,感受不一樣狀況挺難分析下去的,甚至陷入了思惟的僵局。code
若是能讓問題簡化一下該多好呀,沒錯,若是咱們把問題變成咱們之前作過的問題或者容易作的問題,是否能從中發現新的思路?遞歸
假設問題變成:求兩個純字符串進行匹配。實現代碼能夠以下:ip
package main func isMatch(text string, pattern string) bool { if pattern == "" { if text != "" { return false } else { return true } } first_match := false if pattern[0] == text[0] { first_match = true } return first_match && isMatch(text[1:], pattern[1:]) } func main() { text := "abc" pattern := "ab" isMatch(text, pattern) }
這裏用到了遞歸,之因此這麼處理,是爲了後續迭代。
那麼若是再增長一個條件,把「.」符號加上,若是是帶有「.」符號的字符串去匹配一段字符串呢?字符串
須要在實現的時候考慮第一個字節是不是該特殊符號string
func isMatch2(text string, pattern string) bool { if pattern == "" { if text != "" { return false } else { return true } } first_match := false if pattern[0] == text[0] || pattern[0] == '.' { first_match = true } return first_match && isMatch2(text[1:], pattern[1:]) }
能解決「.」符號的狀況,針對「*」符號的狀況,咱們能夠進一步思考。
可能性:進階
func isMatch(text string, pattern string) bool { if pattern == "" { if text != "" { return false } else { return true } } first_match := false text_bool := false if text != "" { text_bool = true } if text_bool && (pattern[0] == text[0] || pattern[0] == '.') { first_match = true } if len(pattern) >=2 && pattern[1] == '*' { return isMatch(text, pattern[2:]) || first_match && isMatch(text[1:], pattern) } else { return first_match && isMatch(text[1:], pattern[1:]) } }
這段代碼都是用遞歸實現的,可是遞歸的時間複雜度消耗更大,徹底能夠考慮將每一次遞歸的結果保存下來,因而咱們又能夠往動態規劃的方向思考。
選擇dp保存結果,dp[i][j]表示前i個字符串被j個字節pattern匹配的結果。循環
func isMatch(s string, p string) bool { memory := make(map[string]bool) return dp(0, 0, memory, s, p) } func dp(i int, j int, memory map[string]bool, s string, p string) bool { iToStr := strconv.Itoa(i) jToStr := strconv.Itoa(j) keyStr := iToStr + "," + jToStr if _, ok := memory[keyStr]; ok { return memory[keyStr] } if j == len(p) { return i == len(s) } first := (i < len(s)) && (p[j] == s[i] || p[j] == '.') var ans bool if j <= (len(p) -2) && p[j+1] == '*' { ans = dp(i, j+2, memory,s, p) || first && dp(i+1, j, memory, s, p) } else { ans = first && dp(i+1, j+1, memory, s, p) } memory[keyStr] = ans return ans }
反思:還有無更好的解法呢?好比把循環放到外層,而不是封裝成dp函數?