程式碼隨想錄 day8|| 151 翻轉單詞 28 字串匹配 459 重複子串

周公瑾55發表於2024-07-25

151 翻轉單詞

func reverseWords(s string) string {
	// 思考: 判斷單詞條件是從0或者空格開始到終或者空格結尾,最簡單方式strings.split之後變成切片,然後反轉就行了
	// 考慮雙指標,左指標指向單詞首位,右指標指向單詞末尾
	var res []byte
	var left, right int
	for right <  len(s) {

		// 題設至少存在一個單詞,所以此處 left < len(s) 條件可以去除
		for left < len(s) && s[left] == ' '{  // 透過字串索引得到的是byte型別,所以應該透過‘’進行對比
			left++
			right = left
		}

		// 找到首位非空字元,單詞的開始
		for right < len(s) && s[right] != ' '{ // 判斷單詞結尾
			right++
		}

		// 插入單詞中間插入空格 s[left:right] + ' '
		// 此處易錯點,字串索引之後是byte型別 ,但是切片操作之後還是字串型別,本質上是一個子字串
		if left == right {
			// 此時相當於單字元 byte
			break
		}
		char := s[left:right] + string(' ')
		res = append([]byte{}, append([]byte(char), res...)...)  // 將區間字元  單詞 插入到結果集開頭
		left = right  // 將left位置重置到下一個單詞開頭
	}
	return string(res[:len(res)-1]) // 去除最後一位的空格
}

// 最佳化的前插方法
word := s[left:right]
if len(res) > 0 {
	res = append([]byte{' '}, res...) // 插入空格
}
res = append([]byte(word), res...)


// 時間,總共遍歷次數n(一次只有一個指標移動,移動完成,另一個跳到當前位置)  空間 n res建立了長度為n的byte切片

28 字串匹配

func strStr(haystack string, needle string) int {
	// 先考慮雙指標思路
	if len(needle) > len(haystack) {
		return -1
	}
	if haystack == needle {
		return 0
	}

	for i := 0; i < len(haystack); i++ {
		if len(haystack[i:]) < len(needle) { // 匹配到後幾位就無需繼續迴圈了
			return -1
		}
		for j := 0; j < len(needle); j++ {
			if haystack[i+j] != needle[j] {
				break
			}

			if j == len(needle) - 1{  // 匹配到末尾了,結束
				return i
			}
		}

	}

	return -1
}
// 暴力雙迴圈辦法,時間m*n  空間 1

KMP 演算法中的字首表邏輯

KMP(Knuth-Morris-Pratt)演算法透過字首表(或部分匹配表)來加速字串匹配過程。其核心思想是利用已經匹配過的部分資訊,避免重複匹配,從而提高效率。以下是為什麼在發生衝突時要取衝突字元前一位索引對應的字首表值(即 next 陣列值)作為新索引進行匹配的數學邏輯解釋:

基本概念

  1. 字首表(next 陣列):對於模式串 ( P ),字首表 next[i] 表示模式串 ( P ) 中從位置 0 到位置 ( i ) 的子串的最長相同字首和字尾的長度。
  2. 衝突:在匹配過程中,如果主串 ( T ) 中的字元 ( T[j] ) 與模式串 ( P[k] ) 不匹配,就發生了衝突。

邏輯推導

假設在匹配過程中,主串 ( T ) 和模式串 ( P ) 已經匹配到了位置 ( j ) 和 ( k ),即 ( T[j] ) 與 ( P[k] ) 發生了衝突。此時我們需要決定下一步如何繼續匹配。

1. 已匹配部分的特點

在匹配過程中,我們已經知道 ( P[ 0 ... k-1] ) 與 ( T[ j-k ... j-1 ] ) 是匹配的。這意味著:

[ T[ j-k ... j-1] = P[0 ... k-1] ]

2. 衝突發生

當 ( T[j] != P[k] ) 時,發生衝突。此時我們需要利用字首表來決定模式串 ( P ) 的下一個匹配位置。

3. 字首表的作用

字首表 next[k] 表示模式串 ( P ) 中從位置 0 到位置 ( k-1 ) 的子串的最長相同字首和字尾的長度。記這個長度為 ( len ),即:

[ len = next[k] ]

這意味著:

[ P[0 ... len-1] = P[k-len ... k-1] ]

4. 跳過不必要的比較

由於 ( P[0 ... len-1] ) 與 ( P[k-len ... k-1] ) 是相同的,我們可以跳過這部分已經匹配的字首,直接從位置 ( len ) 開始繼續匹配。這就是為什麼在發生衝突時,我們將模式串的索引更新為 next[k] 的原因。

數學邏輯

  1. 已匹配部分的字首和字尾相同:在匹配過程中,我們已經匹配了 ( P[0 ... k-1] ) 和 ( T[j-k ... j-1] ),並且 ( P[0 ... len-1] = P[k-len ... k-1] )。
  2. 避免重複匹配:在發生衝突時,如果我們重新從 ( P[0] ) 開始匹配,會導致重複比較已經匹配過的子串。透過字首表,我們可以直接跳到下一個可能的匹配位置,從而避免重複比較。
  3. 遞迴性質:字首表的值本身也是透過遞迴計算得到的,因此在發生衝突時,使用字首表的值作為新的索引可以保證我們跳過的部分是已經匹配的部分,從而保證匹配的正確性。

例子

假設模式串 ( P = \text{"aabaa"} ) 和主串 ( T = \text{"aabaacaabaa"} ),我們在 ( T ) 中找到一個不匹配的位置:

  1. 當前匹配到 ( T[j] ) 和 ( P[k] ),即 ( T[5] = \text{"c"} ) 和 ( P[4] = \text{"a"} ) 不匹配。
  2. 此時 ( k = 4 ),字首表 next[4] = 2,表示 ( P[0 ... 1] ) 與 ( P[2 ... 3] ) 是相同的。
  3. 因此,我們可以跳過前面已經匹配的部分,直接從 ( P[2] ) 開始繼續匹配。

透過這種方式,KMP 演算法利用字首表在匹配過程中高效地跳過不必要的比較,從而提高了匹配效率。

實現程式碼

func nextSlice(s string) []int{
	// kmp演算法計算字首陣列: 最長相等前字尾
	// kmp 思路,匹配到衝突字元時候,取當前字元前一位索引的next陣列值,作為字元索引然後再進行匹配

	// 1, 初始化
	var (
		k int // 儲存當前位最長相等前字尾
		next []int // 字首陣列
	)
	next = make([]int, len(s))

	for i := 1; i < len(s); i++ {
		// 2, 處理字元不相等情況,如果不相等,那麼取當前位置索引的前一位的next陣列值作為索引進行下一步匹配
		for k > 0 && s[i] != s[k] {  // 對比
			// 找索引前一位next陣列值作為索引
			k = next[k-1] // 取新的next值作為索引
		}

		// 3,判斷相等情況
		if s[i] == s[k] {
			k++
		}

		// 4,儲存next陣列
		// next儲存的是最長相等前字尾
		// next[i] = k 表示 s[0:k-1] == s[i-k+1:i],即 s[0:k] 是 s[0:i] 的最長相等前字尾
		// 所以說明k的位置就是代表了最長相等前字尾
		next[i] = k
	}

	return next
}
時間n (不是n^2 原因是並不是完整的兩層迴圈,對於每個字元最多被訪問2次,前進或者回退,所以只是一層遍歷) 空間n
func strStr(haystack string, needle string) int {
	// 雙指標最佳化 kmp演算法(透過字首表實現逐步縮小匹配範圍的作用,達到在大批次匹配效率的最佳化)

	// 初始化next陣列
	next := nextSlice(needle)
	fmt.Println(next)

	var i, j = 0, 0
	for i < len(haystack) {  // tmd一定要先處理不等在處理想等情況,不然每次j都會被重置為0,太痛苦了
		// 考慮不相等情況
		for j > 0 && haystack[i] != needle[j] {
			j = next[j-1]  // 更新為next陣列前一位值作為新索引
		}

		// 考慮相等
		if haystack[i] == needle[j] {
			if j == len(needle) - 1 {
				return i - j
			}
			j++
		}

		i++ // 如果j回退到0仍然不相等,那麼就i往後移動一位繼續與j匹配
	}

	return -1
}
時間 n + m   空間n

459 判斷重複子串

image

//leetcode submit region begin(Prohibit modification and deletion)
func repeatedSubstringPattern(s string) bool {
	// 思考 kmp演算法
	next := nextSlice(s)
	// 分析如果能夠被字串重複構成
	// 最長相等前字尾所不包含的部分就是公共字串, 對於重複串而言最長相等前字尾一定是next(len-1)
	// 如果公共字串能夠被字串長度整除,那麼就說明是重複串

	lenght := len(s)
	if next[lenght - 1] > 0 && lenght % (lenght - next[lenght - 1]) == 0 {
		return true
	}

	return false
}

func nextSlice(s string) []int {
	var (
		k int
		next []int
	)
	// new
	next = make([]int, len(s))

	for i := 1 ; i < len(s) ; i++ {
		// not equal
		for k > 0 && s[i] != s[k] {
			 k = next[k-1]
		}

		// equal
		if s[i] == s[k]{
			k++
		}

		// update next, because s[0:k-1] == s[i-k+1: i]
		next[i] = k
	}
	return next
}
時間m+n 空間n
func repeatedSubstringPattern(s string) bool {
	// 思考 移動匹配法, 透過將s + s然後判斷 news[1:len(news)-2] 範圍內出現過s實現判斷
	// 為什麼區間是 1到length-1 ,因為目的是判斷區間中間是否出現過s,所以要去除兩端端點,防止出現被第一個s或者加到的第二個s匹配
	var news strings.Builder
	news.WriteString(s)
	news.WriteString(s)

	s2 := news.String()
	if strings.Contains(s2[1: len(s2) - 1], s) {
		return true
	}

	return false
}

相關文章