算法題之字符串匹配問題


我最近復習一道困難程度的算法題,發現了許多有趣之處。在借鑒了他人解法后,發現從最簡單的情況反推到原題是一種解鎖新進階的感覺。從遞歸到動態規划,思維上一步一步遞進,如同一部跌宕起伏的小說,記錄下來和諸君共賞之。

題目如下:

給你一個字符串 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)

這是一道關於字符串匹配的問題,其中匹配字符串里面可能含有兩種特殊符號「.」和「*」。

說時候剛拿到這道題的時候我很懵逼,直接動手分析到帶有「*」符號的時候,感覺不同情況挺難分析下去的,甚至陷入了思維的僵局。

如果能讓問題簡化一下該多好呀,沒錯,如果我們把問題變成我們以前做過的問題或者容易做的問題,是否能從中發現新的思路?

假設問題變成:求兩個純字符串進行匹配。實現代碼可以如下:

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)
}

這里用到了遞歸,之所以這么處理,是為了后續迭代。
那么如果再增加一個條件,把「.」符號加上,如果是帶有「.」符號的字符串去匹配一段字符串呢?

需要在實現的時候考慮第一個字節是否是該特殊符號

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:])
}

能解決「.」符號的情況,針對「*」符號的情況,我們可以進一步思考。
可能性:

  • 1.匹配0次。
  • 2.匹配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函數?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM