1. 程式人生 > >KMP算法學習筆記

KMP算法學習筆記

cas 字段 一位 acad lock 繼續 code 新的 代碼

KMP算法 從零開始

部分來自他人博客,蒟蒻只是總結學習

  • 引言
    字符串匹配。給你兩個字符串,尋找其中一個字符串是否包含另一個字符串,如果包含,返回包含的起始位置.
char *str = "bacbababadababacambabacaddababacasdsd";  
char *ptr = "ababaca";
  • 暴力解法

    如果當前字符匹配成功(即S[i] == P[j]),則i++,j++,繼續匹配下一個字符;
    如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j 被置為0。

來看看時間復雜度:最壞情況下為O(n*m)
所以有沒有一種改進的算法

  • 改進方法
    可以實現復雜度為O(m+n),為何簡化了時間復雜度:
    KMP算法主要是取消了指針的回溯,充分利用了目標字符串ptr的性質(比如裏面部分字符串的重復性,即使不存在重復字段,在比較時,實現最大的移動量)。每趟匹配過程中出現字符比較不等時,不回溯主指針i,利用已得到的“部分匹配”結果將模式向右滑動盡可能遠的一段距離,繼續進行比較。
    具體概念上的我不想深究,從代碼開始理解
    ---

    代碼

  • KmpSearch函數
    • 假設現在文本串S匹配到 i 位置,模式串P匹配到 j 位置
      • 如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++,繼續匹配下一個字符;
      • 如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]。此舉意味著失配時,模式串P相對於文本串S向右移動了j - next [j] 位。
        換言之,當匹配失敗時,模式串向右移動的位數為:失配字符所在位置 - 失配字符對應的next 值,即移動的實際位數為:j - next[j],且此值大於等於1。
  • 所以next 數組各值的含義:代表當前字符之前的字符串中,有多大長度的相同前綴後綴。例如如果next [j] = k,代表j 之前的字符串中有最大長度為k 的相同前綴後綴。
    此也意味著在某個字符失配時,該字符對應的next 值會告訴你下一步匹配中,模式串應該跳到哪個位置(跳到next [j] 的位置)。如果next [j] 等於0或-1,則跳到模式串的開頭字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某個字符,而不是跳到開頭,且具體跳過了k 個字符。

int KmpSearch(char* s, char* p)  
{  
    int i = 0;  
    int j = 0;  
    int sLen = strlen(s);  
    int pLen = strlen(p);  
    while (i < sLen && j < pLen)  
    {  
        //①如果j = -1,或者當前字符匹配成功(即S[i] == P[j]),都令i++,j++      
        if (j == -1 || s[i] == p[j])  
        {  
            i++;  
            j++;  
        }  
        else  
        {  
            //②如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]      
            //next[j]即為j所對應的next值        
            j = next[j];  
        }  
    }  
    if (j == pLen)  
        return i - j;  
    else  
        return -1;  
}  

  • getnext()函數
    • (1) next[0] = -1;
    • (2) 設next[j] = k,則next[j+1] = ?
      令j=j+1,k=k+1;
      • 若pk=pj,則有“p1…pk-1pk”=“pj-k+1…pj-1pj” ,
        next[j]=k;
      • 若pk+1≠pj+1,可把求next值問題看成是一個模式匹配問題,整個模式串既是主串,又是子串。
        即使得k=next[k],回溯求得最長前綴等於最長後綴的下標
j 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
模式串 0 a b c a a b b c a b c a a b d a
next[j] -1 0 0 0 1 1 2 0 0 1 2 3 4 5 6 0 1
void getnext(int*next,char*ctr){
    next[0]=-1;
    int j=0,k=-1,len=strlen(ctr);
    while(j<len){
        if(k==-1||ctr[j]==ctr[k]){
            j++;
            k++;
            next[j]=k;
        }
        else k=next[k];
    }
}

完整代碼

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+7;
int a[maxn],b[maxn];
int nxt[10005],n,m;
void getnext(){
    int i=0,j=-1;
    nxt[0]=-1;
    while(i<m){
        if(j==-1||b[i]==b[j]){
            i++,j++;
            if(b[i]==b[j])nxt[i]=nxt[j];
            else nxt[i]=j;
        }
        else j=nxt[j];
    }
}
int kmp(){
    int i=0,j=0;
    getnext();
    while(i<n){
        if(a[i]==b[j]||j==-1)i++,j++;
        else j=nxt[j];
        if(j==m)return i-j+1;
    }
    return -1;
}
int main(){
    int t;
    scanf("%d",&t);
    while(t--){
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++)scanf("%d",&a[i]);
        for(int i=0;i<m;i++)scanf("%d",&b[i]);
        if(n<m)printf("-1\n");
        else printf("%d\n",kmp());
    }
    return 0;
}

練習

  1. 牛客三 E

    題目:給你一個字符串S,你要對字符串S的每一位i將前i位的字符串移動到尾部形成一個新的字符串,如果形成的字符串相同則歸為一類Li。現在讓你將Li類按照字典序排序,並讓你輸出每一類的數量和每一類中字符串對應的下標i

很多人用哈希,正解是KMP的next數組的運用.通過觀察,題目問的就是求字符串的循環節,而next數組就是前後綴的運用。然後有個結論:如果對於一個長度為L的字符串,如果L%(L-next[L])==0則代表它具有循環節,且循環節的長度為L-next[L]。

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+7;
char b[maxn];
int net[maxn],len;
template<class T>
void read(T &res)
{
    res = 0;
    char c = getchar();
    T f = 1;
    while(c < '0' || c > '9')
    {
        if(c == '-') f = -1;
        c = getchar();
    }
    while(c >= '0' && c <= '9')
    {
        res = res * 10 + c - '0';
        c = getchar();
    }
    res *= f;
}
template<class T>
void out(T x)
{
    if(x < 0)
    {
        putchar('-');
        x = -x;
    }
    if(x >= 10)
    {
        out(x / 10);
    }
    putchar('0' + x % 10);
}
void get_next(char*ctr)
{
    net[0]=-1;
    int j=0,k=-1;
    len=strlen(ctr);
    while(j<len)
    {
        if(k==-1||ctr[j]==ctr[k])
        {
            j++;
            k++;
            net[j]=k;
        }
        else k=net[k];
    }
}
int main()
{
    scanf("%s",b);
    get_next(b);
    int tmp=len-net[len];
    if(len%tmp==0&&tmp!=len)
    {
        out(tmp);
        puts(" ");
        for(int i=0; i<tmp; i++)
        {
            out(len/tmp);
            printf(" ");
            for(int j=i;j<len;j+=tmp){

                out(j);
                //
                printf(" ");
            }
            printf("\n");
        }
    }
    else{
        out(len);
   // puts(" ");
         printf("\n");
        for(int i=0;i<len;i++){
                out(1);
                printf(" ");
            out(i);
            printf("\n");
        }

    }
    //
    return 0;
}

KMP算法學習筆記