徹底理解字符串匹配KMP算法
大家好,我是小風(fēng)哥,今天簡(jiǎn)單聊聊字符串匹配kmp算法。
字符串匹配是計(jì)算機(jī)科學(xué)中非?;A(chǔ)的操作,給定兩個(gè)字符串a(chǎn)和b,我們需要判斷字符串a(chǎn)是否包含字符串b。
圖片
像你我這樣的普通程序員能想到的最簡(jiǎn)單方法是這樣的,用字符串b不斷去匹配每個(gè)主串中的子串。
假設(shè)給定這樣兩個(gè)字符串:
圖片
首先從主串的第一個(gè)位置和子串的第一個(gè)位置去匹配,我們發(fā)現(xiàn)A和B不相同:
圖片
因此主串指針后移一位,子串重新從最第一個(gè)字符開(kāi)始匹配。
圖片
這時(shí)我們發(fā)現(xiàn)A和C不同,因此匹配失敗。
圖片
主串指針回退到第三個(gè)字符,子串重新從第一個(gè)字符開(kāi)始匹配。
圖片
此時(shí)B和A又不同,重復(fù)上述過(guò)程。
這次成功找到多個(gè)相同的字符,但最后一個(gè)字符匹配失?。?/p>
圖片
按照我們的算法,主串指針需要回退到第5個(gè)字符重新匹配。
圖片
這就是你我這種肉體凡胎能想到的算法,時(shí)間復(fù)雜度是O(mn),效率低下的原因當(dāng)然是主串指針需要回退。
然而有三位大神不是這么想的,它們跳出來(lái)凡人的思考方式發(fā)明了一種極具創(chuàng)意的算法,由于是三個(gè)人同時(shí)發(fā)現(xiàn),因此這個(gè)算法取了三人名字的首字母,這就是著名的kmp算法。
圖片
看到這里相信你就能明白為什么這個(gè)算法很難掌握了吧,難是正常的,覺(jué)得不難才不正常,如果你能無(wú)師自通搞定kmp算法,那么早出生幾十年你也能和大師們并駕齊驅(qū)供我等凡夫俗子瞻仰。
廢話不多說(shuō),接下來(lái)就讓我們領(lǐng)略一下大師的非凡境界。
注意看這個(gè)主串指針,大師們思考的第一個(gè)問(wèn)題就是,主串指針是否有必要回退,這是最關(guān)鍵最核心的問(wèn)題。
圖片
讓我們回到剛才部分匹配的示例。
主串指針是否需要需要回退呢?我們思考兩種可能。
第一種可能,即使能匹配成功,匹配成功的起始位置也在主串指針H及以后,在這種情況下主串指針不需要回退。
圖片
第二種可能,匹配成功的起始位置經(jīng)過(guò)主串指針H:
圖片
在這種情況下,主串指針之前的兩個(gè)字符A和B一定是成功匹配了的:此時(shí)我們只需要比較主串指針H及以后的位置即可。
圖片
只有這么兩種可能。
因此可以看到,主串指針根本就沒(méi)有必要回退。
現(xiàn)在我們知道了主串指針不需要回退,那么子串指針該從哪里開(kāi)始匹配呢?從頭開(kāi)始嗎?
圖片
注意看我們剛才提到的第二種可能,匹配成功的起始位置經(jīng)過(guò)主串指針H,在這種情況下,主串指針之前的兩個(gè)字符A和B一定是成功匹配了的,這意味著什么呢?
圖片
這意味著AB是這個(gè)字符串的后綴:
圖片
AB是這個(gè)字符串的前綴:
圖片
不要忘了這兩個(gè)字符串是成功匹配了的:
圖片
也就是說(shuō)這是兩個(gè)完全相同的字符串,這就意味著AB是成功匹配字符串的相同前后綴。
圖片
這樣子符串指針也不需要回退到起始位置,而是從共同前后綴的下一個(gè)位置開(kāi)始匹配即可。
圖片
而對(duì)于部分匹配的子串根本不存在共同前后綴的情況,
圖片
我們直接從子串起始位置進(jìn)行匹配。
圖片
可以看到,由于主串指針不回退,這大幅提高了算法的效率。
想要實(shí)現(xiàn)這樣的算法,關(guān)鍵是怎樣計(jì)算出部分匹配子串的共同前后綴。
因此我們來(lái)到了第二個(gè)核心問(wèn)題。
我們以ABCDAB為例來(lái)講解。
這是長(zhǎng)度為1的前后綴,這是長(zhǎng)度為2的前后綴,以此類(lèi)推。
圖片
可以看到,在所有的前后綴中,相同前后綴的最大長(zhǎng)度是2。
圖片
我們記下來(lái)。
實(shí)際上我們需要把所有子串的相同前后綴都計(jì)算出來(lái)。
對(duì)于ABCDA這個(gè)子串來(lái)說(shuō),相同前后綴長(zhǎng)度是1,因?yàn)閮蓚€(gè)A是相同前后綴。
圖片
而對(duì)于ABCD這個(gè)子串來(lái)說(shuō),相同前后綴的長(zhǎng)度是0,也就是沒(méi)有相同的前后綴。
其它也一樣。
這樣我們就到了一個(gè)數(shù)組,通過(guò)查找這個(gè)數(shù)組我們能知道任意子串的共同前后綴長(zhǎng)度。
圖片
這個(gè)數(shù)組在很多資料中被稱(chēng)之為next數(shù)組。
有了next數(shù)組就簡(jiǎn)單了。
假設(shè)此時(shí)我們發(fā)現(xiàn)兩個(gè)指針指向的字符不同,接下來(lái)只需要簡(jiǎn)單查找next數(shù)組:
圖片
發(fā)現(xiàn)已匹配部分的相同前后綴長(zhǎng)度是2:
圖片
因此主指針不動(dòng),子串指針移動(dòng)到相同前后綴的下一個(gè)位置繼續(xù)去匹配即可。
圖片
可以看到,只要我們能得到next數(shù)組,就可以在線性時(shí)間復(fù)雜度內(nèi)解決問(wèn)題。
這里,我們來(lái)到了第三個(gè)核心問(wèn)題,那就是該怎樣高效計(jì)算出next數(shù)組。
假設(shè)此時(shí)我們已經(jīng)計(jì)算出了這個(gè)子串的共同前后綴,也就是長(zhǎng)度為n的這兩個(gè)部分。
圖片
接下來(lái)計(jì)算下一個(gè)位置的最長(zhǎng)前后綴,我們只需要分別后移兩個(gè)指針,然后比較字符是否相等,這里有兩種可能。
第一種可能是接下來(lái)的字符相同,那么這個(gè)子串的最長(zhǎng)相同前后綴的長(zhǎng)度就是n+1。
圖片
然后寫(xiě)到next數(shù)組即可,這很好理解。
圖片
但是如果下一個(gè)位置的字符不相等該怎么辦呢?
注意接下來(lái)是整個(gè)算法最核心的,也是最具技巧的地方。
如果接下來(lái)的兩個(gè)字符不相等,那么前面的這部分絕無(wú)可能形成最長(zhǎng)前后綴。
圖片
因此我們只能找一個(gè)更短的。
圖片
如果能找到一個(gè)更短的,這就意味著這兩部分會(huì)形成一個(gè)共同前后綴。
圖片
然后我們繼續(xù)比較下一個(gè)字符就可以了,這就回到最初的問(wèn)題。
那么這兩部分相同意味著什么呢?
不要忘了紅色部分是我們之前找到一個(gè)共同前后綴,也就是說(shuō)紅色部分的子串完全相同。
圖片
而現(xiàn)在這兩個(gè)子串也相同,這就意味著這兩個(gè)更小的子串其實(shí)是紅色部分子串的最長(zhǎng)前后綴。
圖片
不要忘了,此時(shí)我們的指針已經(jīng)來(lái)到了這里,前面這部分的next數(shù)組已經(jīng)計(jì)算出來(lái)了。
圖片
通過(guò)查next數(shù)組,我們可以快速得到更短前后綴的長(zhǎng)度。
既然紅色部分的長(zhǎng)度是n,那么更短前后綴的長(zhǎng)度其實(shí)就是next[n-1]。
圖片
再來(lái)看下,紅色部分的長(zhǎng)度是n,那么更短前后綴的長(zhǎng)度是next[n-1]。
也就是這個(gè)位置。
圖片
這就是計(jì)算next數(shù)組源代碼中n=next[n-1]這句話的含義。
圖片
現(xiàn)在我們?cè)賮?lái)看一遍整個(gè)過(guò)程。
此時(shí)兩個(gè)字符的長(zhǎng)度不等,那么我們只需要簡(jiǎn)單查一下next[n-1]:
圖片
這就是更短的前后綴長(zhǎng)度,假設(shè)是4。
圖片
此時(shí)前一個(gè)指針回退到位置4,繼續(xù)比較下一個(gè)字符即可。
圖片
如果下一個(gè)字符相同,那么當(dāng)前位置next數(shù)組的值就是n+1。
而如果下一個(gè)字符不相同,我們繼續(xù)查找next[n-1],然后前一個(gè)指針回退,繼續(xù)比較下一個(gè)位置即可。
圖片
這就是完整的kmp算法實(shí)現(xiàn),可以看到整個(gè)代碼實(shí)際上只有30多行。
如果你能在50多年前寫(xiě)出這幾行代碼,你也能和它們并列,在計(jì)算機(jī)科學(xué)史上會(huì)留下你的一筆。
圖片
好啦,以上就是本期全部?jī)?nèi)容。