2020-04-21 字符串匹配KMP

  • KMP 確實很難理解,查找相關資料進行說明:
    以下引用為知乎上作者的文章,

作者:咸魚白
鏈接:https://www.zhihu.com/question/21923021/answer/642165149
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

詳解KMP算法

引子

這幾天自學了KMP算法,也在網上看了很多相關的博文,然而他們對next數組的求解方法的解釋都比較模糊,難于讓讀者理解,故參考幾位前輩的博文,加以優化,撰此博文,分享一下自己的理解。

簡述BF算法

講述KMP算法的原理之前,BF算法是繞不開的話題,也只有了解了BF算法,才能知道KMP算法的優勢。
先來看一個例子:給出兩個字符串A和B,求解A中是否包含B?如果包含,包含了幾個?

<noscript>
image

BF算法的原理是一位一位地比較,比較到失配位的時候,將B串的向后移動一個單位,再從頭一位一位地進行匹配。

如圖2:

</noscript>

image

在比較到第6個字符時(字符索引:5),不匹配了,此時就要將B串后移一個單位,從頭開始匹配(將原本指向A串第六個字符的指針i指向第二個字符,指向B串第六個字符的指針j重新指向B串開頭):

</noscript>

image

然而此時我們一眼可以看出第二次匹配也是必然失敗的,但計算機并不知道,它只會按照BF算法一位一位的比較下去(在很多情況下要比較很多位才能發現不匹配),這種暴力求解的算法效率是極低的,所以我們有沒有辦法讓計算機根據已經匹配過的部分知道自己從頭匹配的時候應該忽略哪些部分,省去不必要的匹配?(在此例中即為從頭跳過第二位的b從第三位開始新的匹配,例子不夠極端,可能并不是很好理解跳過的必要性,請耐心看后續講解)為了解決這個問題,KMP算法便誕生了。

KMP算法

1、前綴與后綴

首先我們要了解幾個概念:前綴、后綴、相同前綴后綴的最大長度(為表述方便,下文均用公共最大長指代),為了直觀一點,我們直接舉例:

abcdef的前綴:a、ab、abc、abcd、abcde(注意:abcdef不是前綴)
abcdef的后綴:f、ef、def、cdef、bcdef(注意:abcdef不是后綴)
abcdef的公共最大長:0(因為其前綴與后綴沒有相同的)
ababa的前綴:a、ab、aba、abab
ababa的后綴:a、ba、aba、baba
ababa的公共最大長:3(因為他們的公共前綴后綴中最長的為aba,長度3)

2、利用相同前綴后綴的最大長度(公共最大長)對匹配過程進行優化

如圖4:

</noscript>

image

index行是字符在B串中的位置索引值。
B串行則記錄了所有字符。
next行則記錄了當前從B串頭部到當前位置的這一子串的公共最大長。(我們先不用管這些公共最大長是如何得到的,暫且假設是上帝告訴了我們)
new行記錄的值則是相應的公共最大長減去1。

好的,我們現在可以再次進行匹配了,還是開頭的例子,B串在第6個字符處(索引5)失配,此時我們可以確認的是B串的前五個字符已經匹配成功了,讓我們根據上面那個表格查找一下已經匹配成功的子串的公共最大長吧(請注意是已經匹配成功的,我們在第6個字符處失配,所以應當去查找第五個字符或者說索引4的位置記錄的公共最大長)

<noscript>
image

</noscript>

image

很明顯,已匹配成功的子串(我們稱之為C串吧)的公共最大長為2,這說明了什么?想一想,B串匹配成功的部分和A串失配處之前的一小部分子串都是C串,C串的公共最大長為2,C串最前面的兩個字符(也就是B串的開頭兩個字符)和C串最后面的兩個字符(也就是A串失配位前面兩個字符)是相同的,這就意味著我們重新進行匹配的時候可以直接將B串的頭部2個字符和A串匹配成功的部分的最后兩個字符對齊。然后開始對比B串的第三個字符與A串的失配字符,進行新一輪的匹配

關于對齊,計算機運行時是怎么做的?我們在匹配時分別用指針i和j指向字符串當前匹配的位置,失配之后指針i不變,繼續指向A串的失配處,指針j則指向B串第三個位置(公共最大長的后面一位,索引為公共最大長)。

如圖6所示:

</noscript>

image

藍色的部分即是通過公共最大長直接匹配的位置,紅色部分是重新開始匹配的位置(兩個指針直接指向的位置),相較于BF算法,我們在這一步跳過了A串的第二個字符“b”,第三個字符“a”,直接將B串頭部對齊了第四個字符,并從B串的第三個字符開始重新與此前失配的字符進行新一輪的匹配。實現就是如此簡單,重新匹配的過程省去了一大堆不必要的匹配,為我們節省了很多時間。

也許有人會疑問,憑什么就應該跳過A串的第三個字符“a”去直接和第四個字符“a”進行匹配呢?難道從第三個開始匹配就不能成功嗎?請回憶一下什么是公共最大長吧?是相同前綴后綴的最大長度。請對照著圖6看完我下面的解釋:我在前面有加粗這么一句話“B串的這部分子串和A串失配處之前的一小部分是相同的”,現在我們假設是從第三個字符“a”開始重新匹配的,如果要與B一直匹配成功到第五個字符“b”,也就是匹配成功了三個字符,這意味著什么?意味著第五個字符位對應的公共最大長應該是3,這顯然是和事實的公共最大長為2是不符合的,以此類推,重新開始匹配時參考公共最大長是合理的最優解。再梳理一下其中的邏輯,現在你應該能理解公共最大長度的意義了。

等一等,我們好像忘了什么?原理懂是懂了,可上帝在哪?我們之前假設上帝告訴了我們next行(next數組中記錄的值),可事實上上帝并不存在,還是得靠我們自己求值鴨!所以,怎么求next數組呢?

3、NEXT(NEW)數組的求解

在此我要先解釋一下new數組中值的意義,正如表示公共最大長的next的數組的值同時表示了對應的最大前綴(為方便表示,我們將最大相同前綴后綴中的前綴稱為最大前綴,最大相同前綴后綴中的后綴稱為最大后綴)的后面一位的索引,new數組中的值等于next數組中的值減去1,我們同時用其表示了對應的最大前綴的最后一位的索引,這是為了后續的程序表達的便利,所以我們此處講解對new數組的求解,因為求出了new數組等于求出來next數組。

先把求解new數組的類c偽代碼貼在這里(B即為B字符串,new即為new數組,不要急于看懂代碼,先看明白我的解釋,代碼看不懂也不礙事,反正是偽代碼):

new[0] = -1
for (int i=1;i<n;i++) /* n為B串長度 */
{
    int j=new[i-1];   /* j為待計算位置前一位對應的new值,也就是最大前綴最后一位對應的索引 */
    while ((B[j+1]!=B[i])&&(j>=0))   /* 任何一個最大前綴后一位與當前求值字符相同時或者向前繼續尋找的索引為-1時停止循環 */
        j=new[j];
    if (B[j+1]==B[i])  /* 字符相同,公共最大長+1,new值+1 */
        new[i]=j+1;
    else               /* 最終尋找到的索引為-1,公共最大長歸零 */
        new[i]=-1;
}

首先,我們可以知道B串的第一個字符對應的公共最大長一定是0,在new數組中則為-1,所以new[0] = -1。接下來,我們不從第二個字符開始類推,而是選取一個位置靠后的具有普適性的例子以便更好的理解推導過程。

如圖7所示:

image

假設我們已經一步步推導得出了前面0-9索引對應的new值,現在要求解索引10對應new值(再次提醒:這個值是公共最大長-1)。

我們首先需要考慮添加了字符b之后的公共最大長是否會增加1,該如何判斷呢?

將索引10對應的字符和前面已經求得解的最長字符串“abaabbabaa”的最大前綴后面一位字符比較,如果二者相同,說明了最大前綴添加一位后產生的字符串和最大后綴添加了字符’b’產生的字符串相同,此時索引10位置對應的公共最大長應該在前面一位的基礎上加1。

那么這個字符串“abaabbaba”的最大前綴的后一位的索引值該如何找到?

這個值是已經求得解的最長字符串的公共最大長的值,即為next[9],或者說是new[9]+1。(next[9]對應了公共最大長的值,也表示著最大前綴后一位的索引)

這個值具體是什么?

是new[9]+1 = 3+1 = 4。我們繼續尋找索引4對應的字符,是’b’,和索引10對應的字符相同,所以索引10對應的公共最大長較之前一位加1,new值加1,所以new[10] = new[9]+1 = 3+1 = 4。

可是,如果B[10]不是’b’呢,如果B[10]=’a’呢?怎么辦?

如圖8所示:

<noscript>
image

很明顯,按之前的推理,在當前情況下,B[10]=’a’和B[4]=’b’是不等的,所以公共最大長不可能增加了,我們只能考慮其與前一位相等甚至減少的情況了,此時該怎么求呢?我們現在要找的是最大前綴的前綴,與“最大后綴加‘a’字符”這一組合的后綴的公共最大長了,我們暫且將索引10前一位對應的子串的最大前綴與最大后綴稱為C串吧(因為二者是相同的字符串),此時我們要求解的問題其實轉化為了C串后面添加一個字符’a’對應的公共最大長,于是我們先利用索引10前面一位索引9對應的new值找到C串(從頭數起)的最后一位,并重復上面的過程來推測此時公共最大長應該朝什么方向變化(這就是代碼中循環的意義),如果向前找到的最后一位的索引是-1,即公共最大長已經減到0的時候,循環終止。現在回頭再看看代碼和注釋,就應當能夠理解了(看懂了的看官們賞個贊再走叭,嚶嚶嚶)。

  • 暴風算法
int matchIndex(char *s ,int sL,char *T,int TL){
    
    int count = 0;
    
    //bf1算法
    for (int i =0; i<= sL-TL; i++) {
        int j = 0;
        for ( j =0; j<TL; j++) {
            count ++;
            if (s[i+j] != T[j]) {
                break;
            }
        }


        if (j == TL) {
            printf("---[%d]\n",count);
            return i;
        }
    }
    printf("---[%d]\n",count);
    return -1;
}
  • rk算法
int jinzhi26(int n){
    int res = 1;
    while (n-->0) {
        res = 26*res;
    }
    return res;
}

int calulateHash(char *s,int start ,int end){
    int add = 0;
    while (start <= end) {
        add += (s[start] - 'a')*jinzhi26(end - start);
        start++;
    }
    return add;
}
//根據上一個hash值計算當前hash值
int calulateHash2(char *s,int start ,int end,int preHash){
    //前一個c*進制,除了第一個,其他都是我們想要的數據,利用了之前已做好的計算,減去不要的,加上最后一個還沒加的
    int add = preHash *jinzhi26(1) - (s[start-1] - 'a')*jinzhi26(end-start+1) + s[end]-'a';
    return add;
}

int matchIndex(char *s ,int sL,char *T,int TL){
    
    int count = 0;
   //rk算法 hash
    
    int target = calulateHash(T, 0, TL);
    int temp = 0 ;
    for (int i =0; i<= sL-TL; i++) {

        if (temp == 0) {
            temp = calulateHash(s, i, i+TL);
        }else{
            temp = calulateHash2(s, i, i+TL, temp);
        }

        if (temp == target) {
            printf("---[%d]\n",count);
            return i;
        }
        count++;

    }
    printf("---[%d]\n",count);
    return -1;
}
  • kmp算法
//沒有相同前綴是-1,有一個是0,以此類推
//如果要換成數組坐標,只需+1即可
void getNext(char* s,int len, int * out){
   
    out[0] = -1;//-1的設計是因為最小共有長度 +1 可以回到第一個字母的下標
    for (int i = 1; i<len; i++) {
        int preNext = out[i-1];
        
        //根據前面已求的長度推斷現在的
        while (s[preNext+1] != s[i] && preNext >=0) {//preNext >=0表示至少有一個相同前綴
            //不相同的話就繼續探索更小的前綴是否可以跟目前這個比較
            preNext = out[preNext];
        }
    
        if (s[preNext+1] == s[i]) {
             out[i] = preNext+1;
        }else{
             out[i] = -1;
            
        }
    }
}


//繼續優化
void getNext2(char* s,int len, int * out){
    
    //    這個是求當前位置的前面字符的共同前后綴
    //0,表示無效,1表示沒有,2表示一個,以此類推
    //第0個用不到,從1開始,1一定是1;
    out[0] = -1;//看了半天才弄明白,原來這里是0,是因為字符串的字符是從第二個開始的,第一個位置是存的長度
    out[1] = 0;
    int i = 0;
    int j = 1;
    
    while (j < len -1) {
       
        if (i == -1 ||s[i] == s[j] ) {
            i++;
            j++;
            out[j] = i;
            
            
            
        }else{
            i = out[i];//回溯,找更小的共同前后綴坐標
        }
    }
    
}

//繼續優化,修正
void getNext3(char* s,int len, int * out){
    
    //    這個是求當前位置的前面字符的共同前后綴
    //0,表示無效,1表示沒有,2表示一個,以此類推
    //第0個用不到,從1開始,1一定是1;
    out[0] = -1;//看了半天才弄明白,原來這里是0,是因為字符串的字符是從第二個開始的,第一個位置是存的長度
    out[1] = 0;
    int i = 0;
    int j = 1;
    
    while (j < len -1) {
       
        if (i == -1 ||s[i] == s[j] ) {
            i++;
            j++;
            
            
            //將接下來的字符串判斷直接在這里判斷,優化下標
            if (s[i] != s[j])  {
                out[j] = i;
            }else{
                //優化重疊部分,理解:如果比較的時候,來到這里,可以多跳幾步重復比較
                out [j] = out[i];
            }
            
            
            
            
            
        }else{
            i = out[i];//回溯,找更小的共同前后綴坐標
        }
    }
    
}


int matchIndex(char *s ,int sL,char *T,int TL){
    
    int count = 0;
 int next[6] = {0};
    getNext3(T, TL, next);//0無效,1.無,2,有一個
       
       int i=0,j=0;
       while (i < sL && j< TL) {
           count++;
           if (s[i] == T[j] || j == 0) {
               i++;
               j++;
           }else{
             j = next[j];//j為跳到第幾個位置下標
            
           }
       }
       
       if (j == TL) {
           printf("---[%d]\n",count);
           return i-TL;
       }

       return -1;
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容