字符串匹配算法(三):KMP(KnuthMorrisPratt)算法
文章目錄
- KMP
- 原理
- next數(shù)組的構(gòu)建
- 代碼實(shí)現(xiàn)
KMP
一提到字符串匹配算法,想必大家腦海中想到的第一個(gè)必然就是KMP算法,KMP算法的全稱叫做KnuthMorrisPratt算法,與上一篇博客中介紹的BM算法一樣,它的核心也是通過(guò)滑動(dòng)來(lái)減少不必要的匹配。
原理
與BM算法不同,KMP算法是從前往后進(jìn)行比較的,并且其將專注點(diǎn)放在已匹配的前綴上,為了方便理解,在這里我們還是使用BM算法中的好前綴和壞字符的概念。
例如下列字符串
還是老樣子,我們?cè)噲D在已匹配的前綴中找到某種規(guī)律,使得我們能夠一下進(jìn)行大量的滑動(dòng),我們此時(shí)就對(duì)好前綴進(jìn)行分析。
此時(shí)我們發(fā)現(xiàn),對(duì)于好前綴來(lái)說(shuō),其存在兩組完全相同的前綴和后綴,分別是aba和a。
此時(shí)我們就可以利用這個(gè)相同的后綴,直接將模式串對(duì)其到后綴的起始位置,為了保證偏移量最大,我們選擇其中最長(zhǎng)的那一組,如下圖
為了表述方便,我將這組前后綴子串分別稱為最長(zhǎng)可匹配前綴子串和最長(zhǎng)可匹配前綴子串
KMP的整體思路就是在已匹配的前綴當(dāng)中尋找到最長(zhǎng)可匹配后綴子串和最長(zhǎng)可匹配前綴子串,在下一輪直接把兩者對(duì)齊,從而實(shí)現(xiàn)模式串的快速滑動(dòng)
next數(shù)組的構(gòu)建
為了避免每次都要去尋找這組最長(zhǎng)匹配的前后綴,我們將其緩存到一個(gè)數(shù)組中,等到使用的時(shí)候再去取,這個(gè)數(shù)組也就是我們經(jīng)常提到的next數(shù)組,而它的構(gòu)建也是KMP算法中最難理解的地方
數(shù)組的下標(biāo)是每個(gè)前綴結(jié)尾字符下標(biāo),數(shù)組的值是這個(gè)前綴的最長(zhǎng)可以匹配前綴子串的結(jié)尾字符下標(biāo)。
那么要如何構(gòu)建next數(shù)組呢?我們需要借助到動(dòng)態(tài)規(guī)劃和回溯的思想,在大部分算法書(shū)和博客中都將這塊描述的十分復(fù)雜,下面我就將其分解成多個(gè)問(wèn)題,來(lái)方便理解
next數(shù)組構(gòu)建時(shí)存在以下兩種情況,為了方便表述,我將后綴起點(diǎn)定為i,前綴起點(diǎn)定為j
前綴和后綴對(duì)應(yīng)的位置匹配
由于第一個(gè)位置只有一個(gè)字符,不存在前綴和后綴一說(shuō),所以初值為-1,其他位置全部初始化為0。從第一個(gè)位置開(kāi)始遍歷。
我們可以采用動(dòng)態(tài)規(guī)劃的方法,如果當(dāng)前位置的前綴和后綴匹配時(shí),則匹配前綴的長(zhǎng)度加一,而我們當(dāng)前的前綴[0, i]又是從[0, i - 1]推導(dǎo)而來(lái),所以它們之間的關(guān)系也就是next[i] = next[i - 1] + 1。
簡(jiǎn)而言之,如果當(dāng)前的字符匹配,則當(dāng)前next的值為上一個(gè)位置的值加一。
前綴和后綴對(duì)應(yīng)的位置不匹配
假設(shè)此時(shí)已匹配前綴為GTGTGC,此時(shí)GTGT與GTGC中的T和C并不相同,又如何處理呢?
此時(shí)我們無(wú)法從上一個(gè)位置來(lái)推出這一個(gè)位置的值,而在這個(gè)壞字符出現(xiàn)之前,GTG又是一組可匹配的最長(zhǎng)前綴子串,所以我們可以將問(wèn)題GTGTGC轉(zhuǎn)換為求后綴GTGC的最長(zhǎng)可匹配前綴
也就是將j給回溯到next[j]的位置
但是由于T和C還是不相同,所以再求后綴GC的最長(zhǎng)可匹配前綴
此時(shí)繼續(xù)回溯
由于G不等于C,所以該位置沒(méi)有任何匹配的前綴,next為0
簡(jiǎn)而言之,如果最長(zhǎng)可匹配后綴子串無(wú)法與前綴匹配,則嘗試尋找當(dāng)前的后綴子串中是否存在一個(gè)可匹配的前綴子串,將j回溯到next[j]的位置
void getNext(const string& pattern, vector<int>& next) {int i , j = 0;next[0] = -1; //第一個(gè)位置不存在數(shù)據(jù),為-1for(int i = 1; i < next.size(); i++){//如果當(dāng)前位置沒(méi)有匹配前綴,則回溯到求當(dāng)前后綴的最長(zhǎng)可匹配前綴while(j != 0 && pattern[j] != pattern[i]){j = next[j];}//如果該位置匹配,則在next數(shù)組在上一個(gè)位置的基礎(chǔ)上加一if(pattern[j] == pattern[i]){j++;}next[i] = j;} }代碼實(shí)現(xiàn)
KMP的完整實(shí)現(xiàn)如下
#include<string> #include<iostream> #include<vector>using namespace std;//獲取next數(shù)組 void getNext(const string& pattern, vector<int>& next) {int i , j = 0;next[0] = -1; //第一個(gè)位置不存在數(shù)據(jù),為-1for(int i = 1; i < next.size(); i++){//如果當(dāng)前位置沒(méi)有匹配前綴,則回溯到求當(dāng)前后綴的最長(zhǎng)可匹配前綴while(j != 0 && pattern[j] != pattern[i]){j = next[j];}//如果該位置匹配,則在next數(shù)組在上一個(gè)位置的基礎(chǔ)上加一if(pattern[j] == pattern[i]){j++;}next[i] = j;} }int knuthMorrisPratt(const string& str, const string& pattern) {//不滿足條件則直接返回falseif(str.empty() || pattern.empty() || str.size() < pattern.size()){return -1;}int i = 0, j = 0;int len1 = str.size(), len2 = pattern.size();vector<int> next(pattern.size(), -1); //next數(shù)組表示第j - 1個(gè)位置的匹配前綴的起始下標(biāo)getNext(pattern, next);for(int i = 0; i < len1; i++){//找到最長(zhǎng)的可匹配前綴while(j > 0 && str[i] != pattern[j]){j = next[j - 1] + 1; //直接滑動(dòng)到匹配的前綴位置,并繼續(xù)匹配下一個(gè)位置}//如果匹配成功,則繼續(xù)匹配下一個(gè)位置if(str[i] == pattern[j]){j++;}if(j == len2){return i - len2 + 1; //返回主串中匹配子串的起始下標(biāo)}}return -1; }空間復(fù)雜度
KMP算法需要借助到一個(gè)next數(shù)組,所以空間復(fù)雜度為O(M),M為模式串長(zhǎng)度
時(shí)間復(fù)雜度
時(shí)間復(fù)雜度主要包含兩個(gè)部分,next數(shù)組的構(gòu)建以及對(duì)主串的遍歷。
其中next數(shù)組構(gòu)建的時(shí)間復(fù)雜度可以估算為O(M),第二步主串的循環(huán)為O(N)。所以KMP算法的時(shí)間復(fù)雜度為O(N + M),N為主串長(zhǎng)度,M為模式串長(zhǎng)度
總結(jié)
以上是生活随笔為你收集整理的字符串匹配算法(三):KMP(KnuthMorrisPratt)算法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 字符串匹配算法(二):BM(BoyerM
- 下一篇: 并发编程中常见的锁机制:乐观锁、悲观锁、