1. 程式人生 > >kmp演算法(最簡單最直觀的理解,看完包會)

kmp演算法(最簡單最直觀的理解,看完包會)

本文將以特殊的方式來讓人們更好地理解kmp演算法,不包括kmp演算法的推導,接下來,我們將從樸素演算法出發。
在這之前,我們先設主串為S,模式串為T,我們要解決的詢問是主串中是否包含模式串(即T是否為S的子串)。
版權宣告:本文為原創文章,轉載請標明出處。

樸素演算法

樸素演算法說白了就是暴力,簡單地講就是先從主串的第一個位置開始逐個對模式串進行匹配,若匹配失敗,則從主串的第二個位置繼續進行匹配,以此類推,直到匹配成功或主串的結尾。
舉個例子1
主串S:aabaaced
模式串T:aac
首先我們會進行這樣的匹配
aabaaced
aac
發現T[0]和S[0]匹配,T[1]和S[1]匹配,而T[2]==c和S[2]==b匹配失敗,接著我們會這樣
aabaaced
  aac
發現T[1]和S[1]匹配,而T[2]==c和S[3]==b匹配失敗,接著
aabaaced
    aac
發現T[2]和S[2]不匹配,繼續
aabaaced
      aac
這次終於成功匹配。
以上所述就是樸素演算法,然而我們再來看一個例子
舉個例子2


主串S:aaaaaaaaaaaaaaaaaaaaab
模式串T:aaaaab
如果這個例子我們還用樸素演算法去匹配,很顯而易見,每次我們都要從頭開始匹配,做法如下
aaaaaaaaaaaaaaaaaaaaab
aaaaab
從T[0]到T[5],對S[0]和S[5]依次進行匹配,發現末尾(T[5]和S[5])沒有匹配,繼續
aaaaaaaaaaaaaaaaaaaaab
  aaaaab
從T[0]到T[5],對S[1]和S[6]依次進行匹配,發現末尾(T[5]和S[6])沒有匹配,繼續
……(此處省略大量的中間過程)
aaaaaaaaaaaaaaaaaaaaab
                             aaaaab
終於匹配成功。
如果用kmp演算法,則過程如下:

aaaaaaaaaaaaaaaaaaaaab
aaaaab
從T[0]到T[5],對S[0]和S[5]依次進行匹配,發現末尾(T[5]和S[5])沒有匹配,繼續
aaaaaaaaaaaaaaaaaaaaab
  aaaaab
直接匹配T[5]和S[6]發現匹配失敗,繼續
……(此處省略大量的中間過程)
aaaaaaaaaaaaaaaaaaaaab
                                  aaaaab
我們發現kmp演算法從第二次匹配開始省略了T[0]到T[4]對S的匹配,因為由kmp演算法我們知道T[0]到T[4]一定已經匹配了,不需要再判斷,那麼kmp演算法是怎麼知道並利用這些資訊的呢,
接下來我們進入正題。

kmp演算法的理解

首先我們從樸素演算法出發,一步一步去引出kmp演算法
主串S:S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
模式串T:T[1]T[2]T[3]T[4]T[5]T[6]
一開始,我們先用樸素演算法進行匹配,得到
S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
T[1]T[2]T[3]T[4]T[5]T[6]
這時候,我們假設前四個匹配成功了,然而S[5]與T[5]匹配失敗,即有
T[1]==S[1]
T[2]==S[2]
T[3]==S[3]
T[4]==S[4]
T[5]!=S[5]
按照樸素演算法的做法,我們應該把T串往右移,得到這樣的式子進行匹配
S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
      T[1]T[2]T[3]T[4]T[5]T[6]
但是這時候我們思考這樣一個問題,將模式串右移一位是否有可能成功匹配??
顯而易見,這樣匹配成功的充要條件是:
T[1]==S[2]
T[2]==S[3]
T[3]==S[4]
T[4]==S[5]
T[5]==S[6]
T[6]==S[7]
結合上次匹配的結果,我們可以把這次匹配成功的充要條件進行變化:
T[1]==S[2]==T[2]
T[2]==S[3]==T[3]
T[3]==S[4]==T[4]
T[4]==S[5]
T[5]==S[6]
T[6]==S[7]
由此我們可以得出一個上次匹配失敗後將模式串T右移一位能夠匹配成功的充要條件:
T[1]==T[2]
T[2]==T[3]
T[3]==T[4]
T[4]==S[5]
T[5]==S[6]
T[6]==S[7]
進而得到上次匹配失敗後將模式串T右移一位能夠過匹配成功的必要條件:
T[1]==T[2]
T[2]==T[3]
T[3]==T[4]
注意,這個必要條件只和模式串T有關!
接著我們討論將模式串右移兩位是否能匹配成功:
S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
             T[1]T[2]T[3]T[4]T[5]T[6]
顯而易見,這樣匹配成功的充要條件是:
T[1]==S[3]
T[2]==S[4]
T[3]==S[5]
T[4]==S[6]
T[5]==S[7]
T[6]==S[8]
結合上次匹配的結果,我們可以把這次匹配成功的充要條件進行變化:
T[1]==S[3]==T[3]
T[2]==S[4]==T[4]
T[3]==S[5]
T[4]==S[6]
T[5]==S[7]
T[6]==S[8]
進而得到上次匹配失敗後將模式串T右移兩位能夠過匹配成功的必要條件:
T[1]==T[3]
T[2]==T[4]
注意,這個必要條件只和模式串T有關!
最後我們討論將模式串右移三位是否能匹配成功:
S[1]S[2]S[3]S[4]S[5]S[6]S[7]S[8]S[9]
                   T[1]T[2]T[3]T[4]T[5]T[6]
顯而易見,這樣匹配成功的充要條件是:
T[1]==S[4]
T[2]==S[5]
T[3]==S[6]
T[4]==S[7]
T[5]==S[8]
T[6]==S[9]
結合上次匹配的結果,我們可以把這次匹配成功的充要條件進行變化:
T[1]==S[4]==T[4]
T[2]==S[5]
T[3]==S[6]
T[4]==S[7]
T[5]==S[8]
T[6]==S[9]
進而得到上次匹配失敗後將模式串T右移三位能夠過匹配成功的必要條件:
T[1]==T[4]
上面討論了三種情況,在第一次匹配到T[5]的時候匹配失敗了,將模式串分別右移動一位,右移動兩位,右移動三位
是否有可能成功
我們這裡設Q為T[1]T[2]T[3]T[4]
可以發現:
右移動一位成功的必要條件是T[1]==T[2],T[2]==T[3],T[3]==T[4],即Q的三個字首等於三個字尾(T[1]T[2]T[3]==T[2]T[3]T[4])

右移動兩位成功的必要條件是T[1]==T[3],T[2]==T[4],即Q的兩個字首等於兩個字尾!(T[1]T[2]==T[3]T[4])

右移動三位成功的必要條件是T[1]==T[4],即Q的一個字首等於一個字尾!
注意,這些移動都只和模式串有關!
這時候,我們可以得出一個結論:
上面這個例子,T[5]是匹配失敗的位置,我們把匹配失敗的位置的前面的所有字元看作一個新的串Q,想要知道右移幾位有可能匹配成功,我們需要討論T[5]前面的字元組成的串Q,如果不滿足Q的三個字首等於三個字尾,我們可以直接跳過右移一位的情況,如果不滿足Q的兩個字首等於兩個字尾,我們可以直接跳過右移兩位的情況,等等,而且,如果一旦滿足,我們在右移後,不需要從模式串的頭部開始匹配,因為如果滿足,前面幾個就已經匹配好了。就比如上面這個例子,若滿足:
T[1]==T[2]
T[2]==T[3]
T[3]==T[4]
我們可以得到右移一位有可能匹配成功,而且因為有上次匹配失敗後留下的資訊
T[2]==S[2]
T[3]==S[3]
T[4]==S[4]
我們可以直接得到
T[1]==T[2]==S[2]
T[2]==T[3]==S[3]
T[3]==T[4]==S[4]
所以直接匹配T[4]和S[5]即可,這麼一來,就是固定主串不動,從匹配失敗的位置開始,判斷模式串需要右移幾位,然後從匹配失敗的位置開始匹配即可,上面那個例子就是T[5]與S[5]匹配失敗,由T[1]T[2]T[3]==T[2]T[3]T[4]可知接下來需要模式串右移一位並匹配T[4]和S[5]。

kmp演算法的使用

在實際使用中,我們不可能匹配失敗一次就去判斷失敗字元前面所有字元組成的串的最長相等的字首和字尾,這樣時間複雜度會很高,所以我們需要在匹配之前對模式串進行預處理,對每個字元如果匹配失敗,要右移幾位進行儲存,在匹配中一旦失敗,直接跳到那個位置就可以了,我們用next陣列進行儲存,比如上面的那個例子,T[5]匹配失敗了,這時候就要讓模式串的指標指向next[5],next[5]是我們在匹配之前就已經預處理過的。
至於如何處理,本文不給予證明,靠下面的幾串程式碼可以實現,讀者自行思考或閱讀書籍或其它文章即可。
獲得next陣列的程式碼如下,T為模式串:

void get_next() {
    next[0] = -1;
    int i = 0, j = -1;
    int len = strlen(T);
    while(i < len) {
        if(j == -1 || T[i] == T[j])
            next[++i] = ++j;
         else
            j = next[j];
    }
}

程式碼很短,其中next[i]代表的是如果在i位置匹配失敗,應該從哪個位置繼續匹配,跟i前面所有字元組成的串Q的字首與字尾有關。注意,這個next陣列是kmp演算法的核心。
接下來給出匹配的過程程式碼:

bool KMP() {
    get_next();
    int len1 = strlen(T);
    int len2 = strlen(S);
    int i = 0, j = 0;           //i指向模式串T,j指向主串S
    while(j < len2) {
        if(T[i] == S[j]) {
            i++;
            j++;
            if(i == len1) {
                return true;
            }
        } else {
            i = next[i];
            if(i == -1) {
                j++;i++;
            }
        }
    }
    return false;
}

kmp演算法的練習建議

理解kmp演算法:poj2752 poj2406 poj1961
常規kmp演算法練習:poj3461 poj2185

如有錯誤或不妥之處,歡迎指正~

相關推薦

kmp演算法(簡單直觀理解

本文將以特殊的方式來讓人們更好地理解kmp演算法,不包括kmp演算法的推導,接下來,我們將從樸素演算法出發。 在這之前,我們先設主串為S,模式串為T,我們要解決的詢問是主串中是否包含模式串(即T是否為S的子串)。 版權宣告:本文為原創文章,轉載請標明出處。

docker學習理解上手

隨著公司專案的增多,有java,nodejs,pathon等專案的部署與開發,不可能在伺服器上裝有各自的環境去適應千奇百怪的開發語言開發的專案,故採用docker來管理這些專案,下面是我學習docker以來自己總結的一些實用的docker命令和乾貨 docker好處: 1.一次打包,到處執行

史上燒腦物理學科普瞬間漲姿勢!

導讀:這是一部壯麗的物理史詩,這是一串光耀後世的姓名。他們是:牛頓,高斯,黎曼,麥克斯韋爾,愛因斯坦,楊振寧,拉馬努金,霍金,維藤……那麼,這些智慧的頭腦到底有多智慧? 我們普遍接受這樣一個結論,即我們現存的這個宇宙起源於一次大爆炸,英文叫做Big Bang!但

【軟體測試 Python自動化】全網全大廠面試題以後你就是面試官!

前言 為了讓大家更好的理解和學習投入到Python自動化來找到一份好的資料也是學習過程中,非常重要的一個點。你的檢索能力越強,你就會越容易找到最合適你的資料。 有需要的小夥伴可以複製群號 313782132 這裡可免費領取! 暗號:部落格。 一、什麼是相容性測試?相容性測試側重哪些方面? 參考答案:

【轉】BBC解剖了一個200斤女子用生命去減肥!

一個 是否 ID 中國 RM 過度 中國人口 理學 enter 此文有部分血腥的解剖畫面,如果無法忍受可以現在關掉頁面!   最近BBC有一部紀錄片震驚全球,   他們用最直觀的方式告訴你肥胖有對可怕,   紀錄片的名字叫——解剖肥胖!▼   紀錄片中,兩位解剖病理學專家

如何實現對函式的隱藏

      當你寫了一個函式可以實現某些功能時,你要給其他人使用,卻不想讓別人看到你寫的函式的內容,這時你就可以通過對函式內容進行隱藏來達到目的,函式提供了對過程的封裝和細節的隱藏,那麼今天我們就來看看如何對函式進行隱藏:   這裡以上篇部落格寫的Sw

十分鐘快速入門 Python不用收藏!

win 等待 變量命名 json stdin 要求 收藏 初始化 mage 本文以 Eric Matthes 的《Python編程:從入門到實踐》為基礎,以有一定其他語言經驗的程序員視角,對書中內容提煉總結,化繁為簡,將這本書的精髓融合成一篇10分鐘能讀完的文章。 讀完本篇

小二乘法的簡單的幾何解釋非常直觀

最小二乘法就是解一個無解的線性方程組 要找到解,就要找到a1,a2的一個線性組合,使得組合後的向量剛好等於b。可惜的是任何的a1和a2線性組合,只可能出現在a1,a2所在的平面S上(這個平面S就是傳說中的向量空間),但是向量b不在平面S上,如下圖。不可能找到解,怎麼辦呢? 無解

ionic 簡單的路由形式頭部固定下面tab切換-------一個簡單的單頁切換起飛了

top log cnblogs .cn inset badge left plus set <ion-header-bar class="bar-dark" align-title="left"> <h1 class="title" >微信 &l

《gate》、《knockout.io》簡單粗暴的遊戲直接了當的io...

漣漪 nal 域名 fps 熱門 自己 png 對手 htm Io遊戲是一款規則簡單休閑的輕競技化遊戲。 國內比較有名的是球球大作戰、蛇蛇爭霸(Slither.io),因其輕量級的在線競技玩法獲得許多玩家青睞。韓國JC娛樂公司新開的FPS網遊《GATE》,亦可以拿來對應

簡單的無線分類無限樹形菜單解決方案

func AD nbsp name 簡單 AC DC pos div JS版本  整體思路就是:不管多少層級,每層都需要添加子類進去,寫個遞歸函數尋找子類即可 var data = [ {"txt":"成都",

【轉載】Oracle sqlplus中簡單的一些命令設置顯示的格式

rac mysql gpo acl 命令格式 lines 屬性 log sql 登錄數據庫:方式(1)當我們剛安裝Oracle數據庫時,登錄賬戶時可以使用win+r 輸入sqlplus,進入sqlplus命令窗口,然後輸入用戶名和密碼,這裏輸入密碼時不會有回顯 方式(2)

使用java實現快速排序(我認為是簡單容易理解的版本

一切都在程式碼和註釋之中。複製貼上就能跑,邊跑邊看才是最愉快的。 所以,話不多說,放碼過來。   public class QuickSort { public static void main(String[] args) { int x[]={6,1,2,7,9,1

PSV破解流程+軟體遊戲安裝(簡單/快的方法整理已測支援3.65~3.68理論上支援全系列版本

1.下載相關工具:下載qcma https://codestation.github.io/qcma/ 下載破解自動打包工具(懶人包,能夠根據使用者AIDKEY自動生成破解包) 新地址連結:https://pan.baidu.com/s/1gjwfsxupsxiCgCWNgC7F8Q

Typescript版本VUE+元件封裝+簡單的策略模式地址選擇

前言 去年做的公司專案,最近再從做升級2.0版本。因為需求是使用者需要填寫五級地址,從省到你家的自然村。 之前一個專案做過,因為在整個專案中只用到了一次,也沒去封裝。 現在使用者新增修改地址用得上,商家註冊,商家員工新增也要用上……那麼多場景我在不封裝要被打死…… 如果不想看文章的可以直接去 gayh

KMP演算法簡單理解 【筆記】

//本文除實現程式碼外全部為原創內容 轉載請註明出處  程式碼來自這裡 kmp演算法是一種改進的字串匹配演算法,由D.E.Knuth與V.R.Pratt和J.H.Morris同時發現,故稱KMP演算法 字串匹配:從字串T中尋找字串P出現的位置(P遠小於T)。其中P稱為“模式”

遞迴實現乘方簡單型別揹包問題組合

目錄 乘方問題 揹包問題  組合 乘方問題   import java.util.Scanner; /** * 遞迴實現乘方問題 * @author Administrator * */ public class Pow{ @Suppres

排查記憶體洩漏簡單直觀的方法

記憶體洩漏無疑會嚴重影響使用者體驗,一些本應該廢棄的資源和物件無法被釋放,導致手機記憶體的浪費,app使用的卡頓,那麼如何排查記憶體洩漏呢? 當然,首先我門有google的官方文件可以參考: 排查記憶體洩漏官方文件 官方文件(二) 大部分部落格的方法

C++11中emplace的簡單初步的理解

emplace是對容器新增元素時的操作,與之前的insert、push_back相比效率更高,簡單的來說就是可以提升容器的插入效率。 vector: emplace  ==  insert emplace_back​  == ​push_back set: em

簡單的單鏈表算是模板吧

大部分是師哥寫的,只有刪除節點部分是我寫的,=。=,菜。。。 品質值得信賴,=。= #include <bits/stdc++.h> using namespace std; struct Node { int data; Node