1. 程式人生 > 實用技巧 >yb課堂 基於瀏覽器和node.js的http客戶端Axios 《三十四》

yb課堂 基於瀏覽器和node.js的http客戶端Axios 《三十四》

KMP演算法

給定文字串A、模式串B,求模式串B在文字串A中出現的次數。

設文字串A的長度為n,模式串B的長度為m

暴力:二重迴圈+回溯 複雜度 O(n*m)

KMP: 將複雜度優化到O(n+m)

本篇文章是我初學KMP演算法所寫,如果有錯誤歡迎指出

另外本文的KMP演算法的實現方式較常規的實現效率似乎低一些,我在網上看到大多數部落格在求prefix陣列時似乎沒有用到遞迴。關於KMP演算法的不同的實現、KMP演算法的擴充套件演算法我會在後期補充進來。

預備知識

字首與真字首

許多題目對於字首與真字首的區分並不嚴格。準確來講,真字首為字首的子集。

如字串ABCD

其字首有A AB ABC ABCD

真字首有A AB ABC

最長公共真前後綴

對於字串AABBAAB

其最長公共真前後綴為AAB (AABBAAB / AABBAAB)

演算法思路介紹

暴力求解的時間浪費在匹配失敗後回溯頭指標的過程中,KMP的本質其實也在頭進行指標回溯,但是KMP能夠使得頭指標儘可能的回溯至少量,從而減少演算法時間。

什麼是prefix陣列

prefix陣列,是指由B串的每一種子串的最長公共真前後綴長度所構成的陣列。(很多人稱prefix陣列為next陣列,二者僅僅是稱呼方法不同,功能是一致的)

prefix[i]即:長度為i的模式串的最長公共前後綴

如模式串abbabbaaba

模式串B的子串 prefix[i] 真字首
a prefix[1] = 0 (null)
ab prefix[2] = 0 (null)
abb prefix[3] = 0 (null)
abba prefix[4] = 1 a
abbab prefix[5] = 2 ab
abbabb prefix[6] = 3 abb
abbabba prefix[7] = 4 abba
abbabbaa prefix[8] = 1 a
abbabbaab prefix[9] = 2 Ab

因此有

prefix: -1 0 0 0 1 2 3 4 1 2

注意,定義prefix[0] = -1 (後面解釋為什麼)

求prefix陣列的目的

使得頭指標儘可能的回溯至少量,從而減少演算法時間。

我們以上圖中的匹配為例。假設當前正在將文字串的b與模式串的a進行匹配。發現二者不相同,此時應該將模式串整體向右移動。對於暴力解法,只會向右移動一格,然後從模式串的第一位開始向後一一匹配。在KMP中,由於我們已經求出了prefix陣列,prefix[6] = 3,這表明,前面長度為6的子串abbabb的前3位和後3位是相同的。由於abbabb這一段是模式串與文字串匹配成功的,因此可以確定文字串b前的6位也應該是abbabb

對於模式串的字首abb,可以將其移動到文字串中原本和模式串的字尾abb匹配的位置,即:

這個操作的複雜度是O(1)的,原因我們後面會講到

如何求prefix陣列

求prefix陣列這件事本身就不是一個容易的過程。如果暴力求解,其複雜度仍然是O(\(m^2\))。一種常見的高效求解prefix陣列的方式是DP

如何得到狀態轉移方程?

  • pattern[i-1] == pattern[prefix[i-1]]

對於模式串abbabbaab

假設已經求出prefix[5] = 2,現在要求prefix[6],只需要檢驗第3個字母(pattern[2])是否和第6位字母(pattern[5])相同。原因在於,已經知道了prefix[5] = 2,即patter[0~1]的ab已經和pattern[3~4]的ab匹配,只需要再檢驗pattern[2]的b和pattern[5]的b匹配,就能得到pattern[0~2]的abb和pattern[3~4]的abb匹配。

因此得到狀態轉移方程之一

if(pattern[i-1] == pattern[prefix[i-1]]){
    prefix[i] = prefix[i-1] + 1;
}
  • pattern[i-1] \(\neq\) pattern[prefix[i-1]]

對於模式串abbabbaab

假設已經求出prefix[0~7]

模式串B的子串 prefix[i] 真字首
a prefix[1] = 0 (null)
ab prefix[2] = 0 (null)
abb prefix[3] = 0 (null)
abba prefix[4] = 1 a
abbab prefix[5] = 2 ab
abbabb prefix[6] = 3 abb
abbabba prefix[7] = 4 abba

現在要求prefix[8]。因為 pattern[7] = a pattern[prefix[7]] = b,二者不相等。故設定比較指標 ptr = prefix[i-1]

while(ptr >= 0 && pattern[i-1] != pattern[ptr]){
		ptr = prefix[ptr];
}

為什麼讓ptr = prefix[ptr] ?

圖中的紫色指標為ptr兩次所指向的位置,分別為 pattern[4] 和 pattern[1]

第一次 ptr = 4 ,將 a 與 pattern[ptr] 比較,其原因是檢驗長度為4的前後綴能否繼續變長

發現二者不同之後,需要找長度較小的前後綴檢驗能否再次變長

由於ptr = 4,因此可以確定的是字首abba與字尾abba匹配

第一個abba的字首a,與第二個abba的字尾相同

因此只需要比較第一個abba的字首a的下一個字母和第二個abba的字尾a的下一個字母

如果相同,迴圈結束;否則重複ptr = prefix[ptr]

不知道是否講清楚了。ptr的變化過程,實際上可以理解為公共前後綴不斷縮短的過程。

最後要麼在縮短到1的過程發現匹配成功,要麼縮短到0(此時ptr = -1)

下面是求prefix陣列的完整程式碼

#define MaxM 20+5
char pattern[MaxM];
int prefix[MaxM];
void get_prefix(int size){
    prefix[0] = -1;
    prefix[1] = 0;
    for (int i = 2; i <= size; i++) {
        if(pattern[i-1] == pattern[prefix[i-1]]){
            prefix[i] = prefix[i-1] + 1;
        }else{
            int ptr = prefix[i-1];
            while (ptr >= 0 && pattern[i-1] != pattern[ptr]) {
                ptr = prefix[ptr];
            }
            if(ptr == -1){
                prefix[i] = 0;
            }else{
                prefix[i] = ptr + 1;
            }
        }
    }
}

開始匹配

這一段的內容與求prefix陣列的目的 其實是一樣的。求prefix陣列的目的就在於降低後期匹配的複雜度。既然知道了其降低複雜度的方式(如果還不明白,請重新看一遍求prefix陣列的目的,本版塊不再重複介紹),寫出匹配的程式碼也就十分容易。

演算法的執行過程中,需要時刻維護兩個指標,ij

i時刻指向文字串的待匹配字元,j時刻指向模式串的待匹配字元

void match(char text[],char pattern[],int text_size,int pattern_size,vector<int>& ans){
    int i = 0,j = 0;
    while (i < text_size && j < pattern_size) {
        
        if(j == -1 || text[i] == pattern[j]){
            i++;
            j++;
        }else{
            j = prefix[j];
        }
        
        if(j == pattern_size){
            ans.push_back(i-j);
            j = prefix[j];
        }
    }
}

完整程式碼

#include <cstdio>
#include <cstring>
#include <vector>

#define MaxN 100000+5
#define MaxM 20+5
using namespace std;
char text[MaxN];
char pattern[MaxM];
int prefix[MaxM];

void get_prefix(int size){
    prefix[0] = -1;
    prefix[1] = 0;
    for (int i = 2; i <= size; i++) {
        if(pattern[i-1] == pattern[prefix[i-1]]){
            prefix[i] = prefix[i-1] + 1;
        }else{
            int ptr = prefix[i-1];
            while (ptr >= 0 && pattern[i-1] != pattern[ptr]) {
                ptr = prefix[ptr];
            }
            if(ptr == -1){
                prefix[i] = 0;
            }else{
                prefix[i] = ptr + 1;
            }
        }
    }
}

void match(char text[],char pattern[],int text_size,int pattern_size,vector<int>& ans){
    int i = 0,j = 0;
    while (i < text_size && j < pattern_size) {
        
        if(j == -1 || text[i] == pattern[j]){
            i++;
            j++;
        }else{
            j = prefix[j];
        }
        
        if(j == pattern_size){
            ans.push_back(i-j);
            j = prefix[j];
        }
    }
}

void KMP(vector<int>& ans){
    scanf("%s",text);
    scanf("%s",pattern);
    int text_size = (int)strlen(text);
    int pattern_size = (int)strlen(pattern);
    get_prefix(pattern_size);
    match(text,pattern,text_size,pattern_size,ans);
}
int main(){
    vector<int> ans;
    KMP(ans);
    int s = (int)ans.size();
    printf("共有%d次匹配\n",s);
    for (int i = 0; i < s; i++) {
        printf("%d\n",ans[i]);
    }
}

測試資料

Input

abbaabbcabbacabbaabbabba

abba

Output

共有5次匹配

0

8

13

17

20