1.需求
先分析下這個需求,這是一個簡單的搜尋功能,在你輸入一段字元後會得到後端返回的搜尋結果,很常見.但是問題是需要將你輸入的字串在搜尋結果中變色,那就要得到子串在父串中的位置.
其實就是在一個字串裡面匹配另一個字串,然後如果匹配成功返回在主串中子串的 startIndex.
2.演算法分析
說完需求,再來看演算法,其實看完這個需求我就想到在 leetCode 上刷過的一道題 連結. 以下圖來分析:
首先遍歷父串,得到與子串中第一個字元相等的 index,然後在遍歷子串,比較子串與父串 index 位之後字元是否相等.如果不相等,那麼繼續遍歷父串得到下一個 index, 直到找到子串匹配父串或者沒找到退出迴圈. 這是個簡單的思路,就直接貼程式碼:/*
impStr()
* Time Complexity: O(nm), Space Complexity: O(n)
*/
func impStr(haystack: String, needle: String) -> Int {
let hChars = Array(haystack.characters)
let nChars = Array(needle.characters)
guard hChars.count >= nChars.count else {
return -1
}
guard nChars.count != 0 else {
return 0
}
for i in 0...(hChars.count - nChars.count) {
// 找到父串中與子串第一個字元相等的 index
if hChars[i] == nChars[0] {
for j in 0..<nChars.count {
// 如果子串某一位和父串不相等,退出迴圈
if hChars[i+j] != nChars[j] {
break
}
// 找到子串匹配父串
if j + 1 == nChars.count {
return i
}
}
}
}
return -1
}
複製程式碼
小需求搞定,easy! 但是...時間複雜度是O(nm)啊...再優化下?
3.演算法優化
再回頭看看上面的分析,在子串某一位匹配失敗後,父串開始下一次迴圈,然後之前已經對齊的子串錯開,那麼父串的匹配必然失敗,在上面的演算法中沒有處理這一塊資訊,如果有一個很長的子串到最後幾位才匹配失敗的話,那這個演算法的效率就非常低下,畢竟是O(nm)的複雜度. 那有什麼辦法的?
KMP演算法
KMP 演算法會利用之前已經匹配成功的部分子串來減少父串的迴圈次數,當子串匹配失敗後,不去讓父串繼續遍歷,而且通過移動子串的 index 來重新開始下一次匹配.
KMP 演算法小解
從上圖來分析下 KMP 的流程,在第一次子串的最後一位D
與父串E
不相等,此時父串ABCDAB
與子串的ABCDAB
是匹配的,此時父串和子串擁有相同的字首AB
,如果父串下次迴圈的其實位置就是AB
時,就是父串的字尾和子串的字首對齊,那麼下一次匹配開始時已經成功匹配了兩個字元,然後繼續匹配.
不難看出,這個演算法充分利用了匹配好的字串,減少了父串迴圈的次數,但是問題是需要去計算匹配成功的字串的是否存在相同的字首與字尾,怎麼計算之後再說,先看子串移動的位數也就是父串迴圈的 index的起始位置的偏移量的計算.
父串向右偏移的位數 = 匹配成功的字串長度 - 已匹配成功的字串的最大公共字首字尾長度
上圖: 父串向右偏移的位數(4) = = 匹配成功的字串長度(6) - 已匹配成功的字串的最大公共字首字尾長度(2)
那就需要另一個演算法來計算一個字串的最大公共字首字尾長度,貼一下演算法:
func getNext(patternStr: String) -> [Int] {
let count = patternStr.characters.count
var k = -1
var next = Array(repeating: -1, count: count)
for var j in stride(from: 0, to: count - 1, by: 1) {
while (k != -1 && patternStr[patternStr.index(patternStr.startIndex, offsetBy: j)] != patternStr[patternStr.index(patternStr.startIndex, offsetBy: k)]) {
k = next[k]
}
k += 1
j += 1
next[j] = k
}
return next
}
複製程式碼
這個函式入參就是子串,得到的一個等於子串 length的字串,每個 index 是除了當前 character 的最大前字尾長度.
字串匹配
計算完成 next陣列之後,接下來就可以用這個陣列去在父串中找到子串的出現位置,假設匹配到 i 位置時,父串匹配了子串的第一個character, 接下來就要比較父串的 i+1和子串的1來匹配,直到出現第j 個位置不匹配,那麼就將子串中0..<j的字串去從 next 陣列中找到最長公共前字尾長度.接下來就講父串的 i偏移 next 陣列中得到的 index,然後繼續匹配.
/*
KMP 演算法 字串匹配
*/
func strStr(_ haystack: String, _ needle: String) -> Int {
guard haystack.characters.count >= needle.characters.count else {
return -1
}
guard needle.characters.count != 0 else {
return 0
}
guard haystack.contains(needle) else {
return -1
}
var indexI = haystack.startIndex
var indexJ = needle.startIndex
var j = 0
let next = getNext(patternStr: needle)
while (indexI < haystack.endIndex && indexJ < needle.endIndex) {
if j == -1 || haystack[indexI] == needle[indexJ] {
j += 1
if indexJ == needle.index(before: needle.endIndex) {
return haystack.distance(from: indexJ, to: indexI)
}
indexI = haystack.index(indexI, offsetBy: 1)
indexJ = needle.index(indexJ, offsetBy: 1)
} else {
let distance = haystack.distance(from: needle.startIndex, to: indexJ)
j = next[distance]
if j == -1 {
indexJ = needle.startIndex
} else {
indexJ = needle.index(needle.startIndex, offsetBy: j)
}
}
}
return -1
}
複製程式碼
KMP 演算法的複雜度是 O(m+n),比之前的O(mn)好多了,基本上這個需求也就可以完美收工了.
4.閒話
其實對客戶端來說,演算法重要不重要呢?能不能用到呢? 其實我的答案是重要,當你遇到一個類似我遇到的這種需求,不管你用了怎麼耗時的演算法完成,從 UI 的表現來說可能都一樣,但是會覺得不夠好,還可以去優化,我覺得這才是能讓自己去不斷進步的一種想法.
5.參考
www.ruanyifeng.com/blog/2013/0… blog.csdn.net/yutianzuiji… github.com/cbangchen/S…