日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

KMP子字符串匹配算法学习笔记

發(fā)布時間:2023/12/13 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 KMP子字符串匹配算法学习笔记 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

文章目錄

    • 學(xué)習(xí)資源
    • 什么是KMP
    • 什么是前綴表
    • 為什么一定要用前綴表
    • 如何計(jì)算前綴表
    • 前綴表有什么問題
    • 使用next數(shù)組來匹配
    • 放碼過來
      • 構(gòu)造next數(shù)組
        • 一、初始化
        • 二、處理前后綴不相同的情況
        • 三、處理前后綴相同的情況
      • 使用next數(shù)組來做匹配
      • 代碼總覽
      • 測試代碼
    • 時間復(fù)雜度分析

學(xué)習(xí)資源

  • 字符串:KMP是時候上場了(一文讀懂系列)- 代碼隨想錄
  • 字符串:都來看看KMP的看家本領(lǐng)!- 代碼隨想錄
  • 什么是KMP

    KMP算法是由這三位學(xué)者發(fā)明的:Knuth,Morris和Pratt,因此,用這三位學(xué)者名字的首字母組合成,來命名該算法。

    KMP主要應(yīng)用在字符串匹配上。KMP的主要思想是當(dāng)出現(xiàn)字符串不匹配時,可以知道一部分之前已經(jīng)匹配的文本內(nèi)容,可以利用這些信息避免從頭再去做匹配了。所以如何記錄已經(jīng)匹配的文本內(nèi)容,是KMP的重點(diǎn),也是next數(shù)組肩負(fù)的重任。

    什么是前綴表

    next數(shù)組就是一個前綴表(prefix table)。

    前綴表是用來回溯的,它記錄了模式串與主串(文本串)不匹配的時候,模式串應(yīng)該從哪里開始重新匹配。

    為了清楚的了解前綴表的來歷,舉一個例子:

    要在文本串:aabaabaafa中查找是否出現(xiàn)過一個模式串:aabaaf。

    如動畫所示:

    動畫里,特意把 子串a(chǎn)a 標(biāo)記上了,這是有原因的,大家先注意一下,后面還會說道。

    可以看出,文本串中第六個字符b 和 模式串的第六個字符f,不匹配了。如果暴力匹配,會發(fā)現(xiàn)不匹配,此時就要從頭匹配了。

    但如果使用前綴表,就不會從頭匹配,而是從上次已經(jīng)匹配的內(nèi)容開始匹配,找到了模式串中第三個字符b繼續(xù)開始匹配。

    此時就要問了前綴表是如何記錄的呢?

    首先要知道前綴表的任務(wù)是當(dāng)前位置匹配失敗,找到之前已經(jīng)匹配上的位置,在重新匹配,此也意味著在某個字符失配時,前綴表會告訴你下一步匹配中,模式串應(yīng)該跳到哪個位置。(MyNote:文本串不用跳轉(zhuǎn))

    那么什么是前綴表:下表i之前(包括i)的字符串中,有多大長度的相同前綴后綴

    (MyNote:本文“下表”的通假于“下標(biāo)”。)

    為什么一定要用前綴表

    前綴表那為啥就能告訴我們 上次匹配的位置,并跳過去呢?

    回顧一下,剛剛匹配的過程在下表5的地方遇到不匹配,模式串是指向f,如圖:

    然后就找到了下表2,指向b,繼續(xù)匹配,如圖:

    以下這句話,對于理解為什么使用前綴表可以告訴我們匹配失敗之后跳到哪里重新匹配 非常重要!

    下表5之前這部分的字符串(也就是字符串a(chǎn)abaa)的最長相等的前綴 和 后綴字符串是 子字符串a(chǎn)a ,因?yàn)檎业搅俗铋L相等的前綴和后綴,匹配失敗的位置是后綴子串的后面,那么我們找到與其相同的前綴的后面從新匹配就可以了。

    所以前綴表具有告訴我們當(dāng)前位置匹配失敗,跳到之前已經(jīng)匹配過的地方的能力。

    如何計(jì)算前綴表

    接下來就要說一說怎么計(jì)算前綴表。如圖:

    一、長度為前1個字符的子串a(chǎn),最長相同前后綴的長度為0。(注意這里計(jì)算相同前后綴,不算重復(fù)的字符)

    二、長度為前2個字符的子串a(chǎn)a,最長相同前后綴的長度為1。

    三、長度為前3個字符的子串a(chǎn)ab,最長相同前后綴的長度為0。

    以此類推:

    四、長度為前4個字符的子串a(chǎn)aba,最長相同前后綴的長度為1。

    五、長度為前5個字符的子串a(chǎn)abaa,最長相同前后綴的長度為2。

    六、長度為前6個字符的子串a(chǎn)abaaf,最長相同前后綴的長度為0。

    那么把求得的最長相同前后綴的長度就是對應(yīng)前綴表的元素,如圖:

    可以看出前綴表里的數(shù)值代表著就是:當(dāng)前位置之前的子串有多大長度相同的前綴后綴

    再來看一下如何利用 前綴表找到 當(dāng)字符不匹配的時候應(yīng)該指針應(yīng)該移動的位置。如動畫所示:

    找到的不匹配的位置, 那么此時我們要看它的前一個字符的前綴表的數(shù)值是多少。

    為什么要看前一個字符的前綴表的數(shù)值呢,因?yàn)橐仪懊孀址淖铋L相同的前綴和后綴。

    所以要看前一位的 前綴表的數(shù)值。

    前一個字符的前綴表的數(shù)值是2, 所有把下表移動到下表2的位置繼續(xù)比配。可以再反復(fù)看一下上面的動畫。

    最后就在文本串中找到了和模式串匹配的子串了。

    前綴表有什么問題

    來看一下剛剛求的這個前綴表有什么問題呢?

    看這個位置紅框的位置,如果要找下表1 所對應(yīng) 前綴表里的數(shù)值的時候,前綴表里的數(shù)值依然是1,然后就要跳到下表1的位置,如此就形成了一個死循環(huán)

    **如何怎么避免呢,就把前綴表里的數(shù)值統(tǒng)一減一, 開始位置設(shè)置為-1 **。 這一點(diǎn)對理解后面KMP代碼很重要!!

    改為如圖所示:

    這樣就避免的死循環(huán),只不過后續(xù)取 前綴表里的數(shù)值的時候,要記得再+1,才是我們想要的值。

    最后得到的新前綴表在KMP算法里通常用一個next數(shù)組來表示。

    注意這個next數(shù)組就根據(jù)模式串求取的。

    使用next數(shù)組來匹配

    有了next數(shù)組,就可以根據(jù)next數(shù)組來 匹配文本串s,和模式串t了。

    注意next數(shù)組是新前綴表(舊前綴表統(tǒng)一減一了)。

    匹配過程動畫如下:

    放碼過來

    下文統(tǒng)稱haystack為文本串, needle為模式串。

    haystack, needle出處。

    構(gòu)造next數(shù)組

    定義一個方法getNext來構(gòu)建next數(shù)組,參數(shù)為一個名為next數(shù)組,和一個字符串。代碼如下:

    private void getNext(int[] next, String s) {}

    構(gòu)造next數(shù)組其實(shí)就是計(jì)算模式串s,前綴表的過程。主要有如下三步:

  • 初始化
  • 處理前后綴不相同的情況
  • 處理前后綴相同的情況
  • 一、初始化

    定義兩個指針i和j:

    • j指向前綴終止位置(嚴(yán)格來說是終止位置減一的位置),
    • i指向后綴終止位置(與j同理)。

    (通常是先i后j,為什么這里相反,接下來看代碼就清楚了。)

    然后還要對next數(shù)組進(jìn)行初始化賦值,如下:

    int j = -1; next[0] = j;
    • j 初始化為 -1原因是前文說過前綴表要統(tǒng)一減一的操作(避免死循環(huán)得情況),所以j初始化為-1。

    • next[] 表示 i(包括i)之前最長相等的前后綴長度(其實(shí)就是j),next[0]初始化為j 。

    二、處理前后綴不相同的情況

    因?yàn)閖初始化為-1,那么i就從1開始,進(jìn)行s[i] 與 s[j+1]的比較。(這里可能一開始不適應(yīng)理解,不用急。)

    所以遍歷模式串s的循環(huán)下表i 要從 1開始,代碼如下:

    for(int i = 1; i < s.length(); i++) { // 注意i從1開始

    如果 s[i] 與 s[j+1]不相同,也就是遇到 前后綴末尾不相同的情況,就要回退。

    如何回退?next[j]就是記錄著j(包括j)之前的子串的相同前后綴的長度。

    那么 s[i] 與 s[j+1] 不相同,就要找 j+1前一個元素在next數(shù)組里的值(就是next[j])。

    所以,處理前后綴不相同的情況代碼如下:

    while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后綴不相同了j = next[j]; // 回退 }

    三、處理前后綴相同的情況

    如果s[i] 與 s[j + 1] 相同,那么就同時向后移動i 和j 說明找到了相同的前后綴,同時還要將j(前綴的長度)賦給next[i], 因?yàn)閚ext[i]要記錄相同前后綴的長度。

    代碼如下:

    if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后綴j++; } next[i] = j; // 將j(前綴的長度)賦給next[i]

    最后整體構(gòu)建next數(shù)組的函數(shù)代碼如下:

    private void getNext(int[] next, String s) {int j = -1;next[0] = j;for(int i = 1; i < s.length(); i++) { // 注意i從1開始while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后綴不相同了j = next[j]; // 向前回溯}if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后綴j++;}next[i] = j; // 將j(前綴的長度)賦給next[i]} }

    代碼構(gòu)造next數(shù)組的邏輯流程動畫如下:

    得到了next數(shù)組之后,就開始用它做匹配。

    使用next數(shù)組來做匹配

    在文本串haystack里找是否出現(xiàn)過模式串needle。定義兩個下表j 指向模式串起始位置,i指向文本串其實(shí)位置。

    那么j初始值依然為-1,這是因?yàn)?strong>next數(shù)組里記錄的起始位置為-1。

    i就從0開始,遍歷文本串,代碼如下:

    for (int i = 0; i < haystack.length(); i++) { // 注意i就從0開始

    接下來就是 haystack.charAt(i) 與 needle.charAt(j + 1) (因?yàn)閖從-1開始的) 進(jìn)行比較。

    如果 haystack.charAt(i) 與 needle.charAt(j + 1) 不相同,j就要從next數(shù)組里尋找下一個匹配的位置。

    代碼如下:

    while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) { // 不匹配j = next[j]; // j 尋找之前匹配的位置 }

    如果 haystack.charAt(i) 與 needle.charAt(j + 1) 相同,那么i 和 j 同時向后移動, 代碼如下:

    if (haystack.charAt(i) == needle.charAt(j + 1)) { // 匹配,j和i同時向后移動 j++; }

    如果j指向了模式串t的末尾,那么就說明模式串t完全匹配文本串s里的某個子串了。

    本題要在文本串字符串中找出模式串出現(xiàn)的第一個位置(從0開始),所以返回當(dāng)前在文本串匹配模式串的位置i 減去 模式串的長度,就是文本串字符串中出現(xiàn)模式串的第一個位置。

    代碼如下:

    if (j == (needle.length() - 1) ) { // 文本串s里出現(xiàn)了模式串treturn (i - needle.length() + 1); }

    代碼總覽

    public class KMP {private void getNext(int[] next, String s) {int j = -1;next[0] = j;for(int i = 1; i < s.length(); i++) { // 注意i從1開始while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后綴不相同了j = next[j]; // 向前回溯}if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后綴j++;}next[i] = j; // 將j(前綴的長度)賦給next[i]}}public int strStr(String haystack, String needle) {if (needle.length() == 0) {return 0;}int[] next = new int[needle.length()];getNext(next, needle);int j = -1; // // 因?yàn)閚ext數(shù)組里記錄的起始位置為-1for (int i = 0; i < haystack.length(); i++) { // 注意i就從0開始while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) { // 不匹配j = next[j]; // j 尋找之前匹配的位置}if (haystack.charAt(i) == needle.charAt(j + 1)) { // 匹配,j和i同時向后移動 j++; }if (j == (needle.length() - 1) ) { // 文本串s里出現(xiàn)了模式串treturn (i - needle.length() + 1); }}return -1;} }

    測試代碼

    import static org.junit.Assert.*;import org.junit.Test;public class KMPTest {@Testpublic void test() {KMP k = new KMP();assertEquals(2, k.strStr("hello", "ll"));assertEquals(-1, k.strStr("aaaaa", "bba"));assertEquals(3, k.strStr("aabaabaafa", "aabaaf"));}}

    時間復(fù)雜度分析

    假設(shè)文本串長度為n,模式串長度為m。因?yàn)樵谄ヅ涞倪^程中,根據(jù)前綴表不斷調(diào)整匹配的位置,可以看出匹配的過程是O(n),但之前還要單獨(dú)生成next數(shù)組,時間復(fù)雜度是O(m),所以整個KMP算法的時間復(fù)雜度是O(n+m)的。

    暴力的解法顯而易見是O(n * m),所以KMP在字符串匹配中極大的提高的搜索的效率

    總結(jié)

    以上是生活随笔為你收集整理的KMP子字符串匹配算法学习笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。