【算法圖文動畫詳解系列】KMP 字串匹配搜索算法

問題描述:字串匹配搜索

假設(shè)現(xiàn)在我們面臨這樣一個問題:有一個文本串S日杈,和一個模式串P遣铝,現(xiàn)在要查找P在S中的位置,怎么查找呢莉擒?

暴力匹配算法

如果用暴力匹配的思路酿炸,并假設(shè)現(xiàn)在文本串S匹配到 i 位置,模式串P匹配到 j 位置涨冀,則有:

1填硕、如果當(dāng)前字符匹配成功(即S[i] == P[j]),則i++鹿鳖,j++扁眯,繼續(xù)匹配下一個字符;

2栓辜、如果失配(即S[i]! = P[j])恋拍,令i = i - (j - 1)垛孔,j = 0藕甩。相當(dāng)于每次匹配失敗時,i 回溯,j 被置為0狭莱。

理清楚了暴力匹配算法的流程及內(nèi)在的邏輯僵娃,咱們可以寫出暴力匹配的代碼,如下:

int ViolentMatch(char* s, char* p)
{
    int sLen = strlen(s);
    int pLen = strlen(p);
 
    int i = 0;
    int j = 0;
    while (i < sLen && j < pLen)
    {
        if (s[i] == p[j])
        {
            //①如果當(dāng)前字符匹配成功(即S[i] == P[j])腋妙,則i++默怨,j++    
            i++;
            j++;
        }
        else
        {
            //②如果失配(即S[i]! = P[j]),令i = i - (j - 1)骤素,j = 0    
            i = i - j + 1;
            j = 0;
        }
    }
    //匹配成功匙睹,返回模式串p在文本串s中的位置,否則返回-1
    if (j == pLen)
        return i - j;
    else
        return -1;
}

KMP 算法

Knuth-Morris-Pratt 字符串查找算法济竹,簡稱為 “KMP算法”痕檬,常用于在一個文本串S內(nèi)查找一個模式串P 的出現(xiàn)位置,這個算法由Donald Knuth送浊、Vaughan Pratt梦谜、James H. Morris三人于1977年聯(lián)合發(fā)表,故取這3人的姓氏命名此算法袭景。

The algorithm of Knuth, Morris and Pratt [KMP 77] makes use of the information gained by previous symbol comparisons. It never re-compares a text symbol that has matched a pattern symbol. As a result, the complexity of the searching phase of the Knuth-Morris-Pratt algorithm is in O(n).
However, a preprocessing of the pattern is necessary in order to analyze its structure. The preprocessing phase has a complexity of O(m). Since mless or equaln, the overall complexity of the Knuth-Morris-Pratt algorithm is in O(n).

KMP 算法核心原理示意圖

求解前綴表的核心思想

把前綴 P[0:j] 當(dāng)成是 P 的模式串(P[0:i] )唁桩,P 本身當(dāng)成是查找的文本。

next 前綴表數(shù)組耸棒,上圖中是 lps 數(shù)組荒澡。

KMP源代碼

極簡版本的 KMP 算法源代碼:

next數(shù)組首位用-1來填充,這樣在處理長度的時候榆纽,思維上不會很繞仰猖。

/**
 * getNext (pattern) 函數(shù): 計算字符串 pattern 的最大公共前后綴的長度 (max common prefix suffix length)
 */
fun getNext(P: String): IntArray {
    val M = P.length
    val next = IntArray(M + 1, { -1 })
    // i: current index of P
    var i = 0
    // j: current index of the longest prefix of P
    var j = -1
    next[0] = -1 // next[i] = j

    // compute next[i]
    while (i < M) {
        // 如果當(dāng)前字符匹配失敗(即P[i] != P[j]) && j != 0 奈籽,則令 i 不變饥侵,j = next[j]。
        // 此舉意味著失配時衣屏,"模式串"即前綴P[0:j], 不再從 0 位置開始比對,直接從 j = next [j] 位置開始比對躏升。
        while (j >= 0 && P[i] != P[j]) {
            j = next[j]
        }
        i++
        j++
        next[i] = j
    }
    return next
}


/**
 * kmp substring search algorithm
 * @param S : the source text string
 * @param P : the search pattern string
 */
fun kmp(S: String, P: String): Int {
    val N = S.length
    val M = P.length

    if (P.isEmpty()) {
        return 0
    }

    // j: the current index of P
    var j = 0
    // i: the current index of T
    var i = 0
    // next array
    val next = getNext(P)

    while (i < N) {
        while (j >= 0 && S[i] != P[j]) {
            j = next[j]
        }
        i++
        j++
        // when j == M, then pattern is founded in text, return the index (i - j)
        if (j == M) {
            return i - j
        }
    }
    return -1
}

fun main() {
    var text = "addaabbcaabffffggghhddabcdaaabbbaab"
    var pattern = "aabbcaab"
    print("${getNext(pattern).joinToString { it.toString() }} \n")

    var index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

    text = "hello"
    pattern = "ll"
    print("${getNext(pattern).joinToString { it.toString() }} \n")

    index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

    text = "abbbbbbcccddddaabaacabdcddaabbbbaad"
    pattern = "aabaacab"
    print("${getNext(pattern).joinToString { it.toString() }} \n")

    index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

}

// 輸出:
//-1, 0, 1, 0, 0, 0, 1, 2, 3
//aabbcaab is the substring of addaabbcaabffffggghhddabcdaaabbbaab, the index is: 3
//-1, 0, 1
//ll is the substring of hello, the index is: 2
//-1, 0, 1, 0, 1, 2, 0, 1, 0
//aabaacab is the substring of abbbbbbcccddddaabaacabdcddaabbbbaad, the index is: 14

另外一個版本代碼:

/**
 * getNext (pattern) 函數(shù): 計算字符串 pattern 的最大公共前后綴的長度 (max common prefix suffix length)
 */
fun getNext(P: String): IntArray {

    val M = P.length
    val next = IntArray(M, { -1 })

    // i: current index of P
    var i = 1
    // j: current index of the longest prefix of P
    var j = 0

    next[0] = 0
    // compute next[i]
    while (i < M) {
        if (P[i] == P[j]) { // ①
            val len = j + 1
            next[i] = len
            i++
            j++
        } else {
            // 如果當(dāng)前字符匹配失敗(即P[i] != P[j]) && j != 0 狼忱,則令 i 不變膨疏,j = next[j-1]。
            // 此舉意味著失配時钻弄,"模式串"即前綴P[0:j], 不再從 0 位置開始比對,直接從 next [j-1] 位置開始比對佃却。
            if (j != 0) {
                j = next[j - 1] // j shift left, jmp ①
            } else {
                next[i] = 0 // now j is 0, next i
                i++
            }
        }
    }

    return next
}


/**
 * kmp substring search algorithm
 * @param S : the source text string
 * @param P : the search pattern string
 */
fun kmp(S: String, P: String): Int {
    val N = S.length
    val M = P.length

    if (P.isEmpty()) {
        return 0
    }

    // j: the current index of P
    var j = 0
    // i: the current index of T
    var i = 0
    // next array
    val next = getNext(P)

    while (i < N - M + 1) {
        if (S[i] == P[j]) {
            i++
            j++
        } else {
            if (j > 0) {
                // 當(dāng)前字符匹配失敗(即S[i] != P[j])窘俺,則令 i 不變饲帅,j = next[j-1]。
                // 此舉意味著失配時,模式串P 不再從 0 位置開始比對,直接從 next [j-1] 位置開始比對灶泵。
                j = next[j - 1]
            } else {
                i++
            }
        }

        // when j == M, then pattern is founded in text
        if (j == M) {
            return i - M
        }
    }

    return -1
}

fun main() {
    var text = "addaabbcaabffffggghhddabcdaaabbbaab"
    var pattern = "aabbcaab"
    print("${getNext(pattern).joinToString { it.toString() }} \n")

    var index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

    text = "hello"
    pattern = "ll"
    print("${getNext(pattern).joinToString { it.toString() }} \n")

    index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

    text = "abbbbbbcccddddaabaacabdcddaabbbbaad"
    pattern = "aabaacab"
    print("${getNext(pattern).joinToString { it.toString() }} \n")

    index = kmp(text, pattern)
    println("$pattern is the substring of $text, the index is: $index")

}

// 輸出:
//0, 1, 0, 0, 0, 1, 2, 3
//aabbcaab is the substring of addaabbcaabffffggghhddabcdaaabbbaab, the index is: 3
//0, 1
//ll is the substring of hello, the index is: 2
//0, 1, 0, 1, 2, 0, 1, 0
//aabaacab is the substring of abbbbbbcccddddaabaacabdcddaabbbbaad, the index is: 14

參考資料

https://www.inf.hs-flensburg.de/lang/algorithmen/pattern/kmpen.htm
https://blog.csdn.net/v_july_v/article/details/7041827

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末育八,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赦邻,更是在濱河造成了極大的恐慌髓棋,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惶洲,死亡現(xiàn)場離奇詭異按声,居然都是意外死亡,警方通過查閱死者的電腦和手機恬吕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門儒喊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人币呵,你說我怎么就攤上這事怀愧。” “怎么了余赢?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵芯义,是天一觀的道長。 經(jīng)常有香客問我妻柒,道長扛拨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任举塔,我火速辦了婚禮绑警,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘央渣。我一直安慰自己计盒,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布芽丹。 她就那樣靜靜地躺著北启,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拔第。 梳的紋絲不亂的頭發(fā)上咕村,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機與錄音蚊俺,去河邊找鬼懈涛。 笑死,一個胖子當(dāng)著我的面吹牛泳猬,可吹牛的內(nèi)容都是我干的批钠。 我是一名探鬼主播泣港,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼价匠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起呛每,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤踩窖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后晨横,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洋腮,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年手形,在試婚紗的時候發(fā)現(xiàn)自己被綠了啥供。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡库糠,死狀恐怖伙狐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瞬欧,我是刑警寧澤贷屎,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站艘虎,受9級特大地震影響唉侄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜野建,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一属划、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧候生,春花似錦同眯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肿孵,卻和暖如春唠粥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背停做。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工晤愧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛉腌。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓官份,卻偏偏與公主長得像只厘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子舅巷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,629評論 2 354

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