一個(gè)需求引發(fā)的算法及優(yōu)化(KMP算法)

1.需求

搜索3.jpg

先分析下這個(gè)需求,這是一個(gè)簡(jiǎn)單的搜索功能,在你輸入一段字符后會(huì)得到后端返回的搜索結(jié)果,很常見.但是問題是需要將你輸入的字符串在搜索結(jié)果中變色,那就要得到子串在父串中的位置.

其實(shí)就是在一個(gè)字符串里面匹配另一個(gè)字符串,然后如果匹配成功返回在主串中子串的 startIndex.

2.算法分析

說完需求,再來看算法,其實(shí)看完這個(gè)需求我就想到在 leetCode 上刷過的一道題 鏈接.
以下圖來分析:

1.png

首先遍歷父串,得到與子串中第一個(gè)字符相等的 index,然后在遍歷子串,比較子串與父串 index 位之后字符是否相等.如果不相等,那么繼續(xù)遍歷父串得到下一個(gè) index, 直到找到子串匹配父串或者沒找到退出循環(huán).
這是個(gè)簡(jiǎn)單的思路,就直接貼代碼:

/*
 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) {
        // 找到父串中與子串第一個(gè)字符相等的 index
        if hChars[i] == nChars[0] {
            for j in 0..<nChars.count {
                // 如果子串某一位和父串不相等,退出循環(huán)
                if hChars[i+j] != nChars[j] {
                    break
                }
                // 找到子串匹配父串
                if j + 1 == nChars.count {
                    return i
                }
            }
        }
    }
    return -1
}

小需求搞定,easy!
但是...時(shí)間復(fù)雜度是O(nm)啊...再優(yōu)化下?

3.算法優(yōu)化

再回頭看看上面的分析,在子串某一位匹配失敗后,父串開始下一次循環(huán),然后之前已經(jīng)對(duì)齊的子串錯(cuò)開,那么父串的匹配必然失敗,在上面的算法中沒有處理這一塊信息,如果有一個(gè)很長(zhǎng)的子串到最后幾位才匹配失敗的話,那這個(gè)算法的效率就非常低下,畢竟是O(nm)的復(fù)雜度.
那有什么辦法的?

KMP算法

KMP 算法會(huì)利用之前已經(jīng)匹配成功的部分子串來減少父串的循環(huán)次數(shù),當(dāng)子串匹配失敗后,不去讓父串繼續(xù)遍歷,而且通過移動(dòng)子串的 index 來重新開始下一次匹配.

KMP 算法小解

2.png

從上圖來分析下 KMP 的流程,在第一次子串的最后一位D與父串E不相等,此時(shí)父串ABCDAB與子串的ABCDAB是匹配的,此時(shí)父串和子串擁有相同的前綴AB,如果父串下次循環(huán)的其實(shí)位置就是AB時(shí),就是父串的后綴和子串的前綴對(duì)齊,那么下一次匹配開始時(shí)已經(jīng)成功匹配了兩個(gè)字符,然后繼續(xù)匹配.
不難看出,這個(gè)算法充分利用了匹配好的字符串,減少了父串循環(huán)的次數(shù),但是問題是需要去計(jì)算匹配成功的字符串的是否存在相同的前綴與后綴,怎么計(jì)算之后再說,先看子串移動(dòng)的位數(shù)也就是父串循環(huán)的 index的起始位置的偏移量的計(jì)算.
父串向右偏移的位數(shù) = 匹配成功的字符串長(zhǎng)度 - 已匹配成功的字符串的最大公共前綴后綴長(zhǎng)度
上圖: 父串向右偏移的位數(shù)(4) = = 匹配成功的字符串長(zhǎng)度(6) - 已匹配成功的字符串的最大公共前綴后綴長(zhǎng)度(2)
那就需要另一個(gè)算法來計(jì)算一個(gè)字符串的最大公共前綴后綴長(zhǎng)度,貼一下算法:

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
}

這個(gè)函數(shù)入?yún)⒕褪亲哟?得到的一個(gè)等于子串 length的字符串,每個(gè) index 是除了當(dāng)前 character 的最大前后綴長(zhǎng)度.

字符串匹配

計(jì)算完成 next數(shù)組之后,接下來就可以用這個(gè)數(shù)組去在父串中找到子串的出現(xiàn)位置,假設(shè)匹配到 i 位置時(shí),父串匹配了子串的第一個(gè)character, 接下來就要比較父串的 i+1和子串的1來匹配,直到出現(xiàn)第j 個(gè)位置不匹配,那么就將子串中0..<j的字符串去從 next 數(shù)組中找到最長(zhǎng)公共前后綴長(zhǎng)度.接下來就講父串的 i偏移 next 數(shù)組中得到的 index,然后繼續(xù)匹配.

   /*
     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 算法的復(fù)雜度是 O(m+n),比之前的O(mn)好多了,基本上這個(gè)需求也就可以完美收工了.

4.閑話

其實(shí)對(duì)客戶端來說,算法重要不重要呢?能不能用到呢?
其實(shí)我的答案是重要,當(dāng)你遇到一個(gè)類似我遇到的這種需求,不管你用了怎么耗時(shí)的算法完成,從 UI 的表現(xiàn)來說可能都一樣,但是會(huì)覺得不夠好,還可以去優(yōu)化,我覺得這才是能讓自己去不斷進(jìn)步的一種想法.

5.參考

http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
http://blog.csdn.net/yutianzuijin/article/details/11954939/
https://github.com/cbangchen/SwiftAlgorithms

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子咏窿,更是在濱河造成了極大的恐慌,老刑警劉巖陡蝇,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痊臭,死亡現(xiàn)場(chǎng)離奇詭異哮肚,居然都是意外死亡登夫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門允趟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恼策,“玉大人,你說我怎么就攤上這事潮剪』量” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵抗碰,是天一觀的道長(zhǎng)狮斗。 經(jīng)常有香客問我,道長(zhǎng)弧蝇,這世上最難降的妖魔是什么碳褒? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮看疗,結(jié)果婚禮上沙峻,老公的妹妹穿的比我還像新娘。我一直安慰自己两芳,他們只是感情好摔寨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著怖辆,像睡著了一般是复。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上竖螃,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天淑廊,我揣著相機(jī)與錄音,去河邊找鬼斑鼻。 笑死蒋纬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坚弱。 我是一名探鬼主播蜀备,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼荒叶!你這毒婦竟也來了碾阁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤些楣,失蹤者是張志新(化名)和其女友劉穎脂凶,沒想到半個(gè)月后宪睹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蚕钦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年亭病,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘶居。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡罪帖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出邮屁,到底是詐尸還是另有隱情整袁,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布佑吝,位于F島的核電站坐昙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏芋忿。R本人自食惡果不足惜炸客,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盗飒。 院中可真熱鬧嚷量,春花似錦、人聲如沸逆趣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宣渗。三九已至抖所,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痕囱,已是汗流浹背田轧。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鞍恢,地道東北人傻粘。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像帮掉,于是被迫代替她去往敵國(guó)和親弦悉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容