KMP算法及優化

KMP算法及優化

今天看到同學在複習數據結構書上的KMP算法,忽然發覺自己又把KMP算法忘掉了,以前就已經忘過一次,看樣子還是沒有真正的掌握它,這回學聰明點,再次搞明白後記錄下來。

一般字符串匹配過程

KMP算法是字符串匹配算法的一種改進版,一般的字符串匹配算法是:從主串(目標字符串)模式串(待匹配字符串)的第一個字符開始比較,如果相等則繼續匹配下一個字符, 如果不相等則從主串的下一個字符開始匹配,直到模式串被匹配完,則匹配成功,或主串被匹配完且模式串未匹配完,則匹配失敗。匹配過程入下圖:
在這裏插入圖片描述

這種實現方式是最簡單的, 但也是低效的,因爲第三次匹配結束後的第四次和第五次是沒有必要的。

分析

第三次匹配在j = 0(a)和i = 2(a)處開始,在j = 4( c )和i = 6(b)處失敗,這意味着模式串和主串中:j = 0(a)和i = 2(a)、j = 1(b) 和 i = 3(b)、j = 2( c )和 i = 4( c )、j = 3(a) 和 i = 5( a )這四個字符相互匹配。

分析模式串的前3個字符:模式串的第一個字符j = 0是a,j = 1(b)、j = 2( c )這兩個字符和j = 0 (a )不同,因此以這兩個字符開頭的匹配必定失敗,在第三次匹配中,主串中i = 3(b)、i = 4( c )和模式串j = 1(b)、j = 2( c )相互匹配,因此匹配失敗後,可以直接跳過主串中i = 3(b)、i = 4( c )這兩個字符的匹配。

繼續分析模式串的 j = 3( a ) 和 j = 4( c ) 這兩個字符,如果模式串匹配到j = 4( c )這個字符才失敗的話,因爲j = 4©的前一個字符j = 3(a)和第一個字符 j = 0( a )是相同的,結合上一個分析得知:

1):下一次匹配中主串已經跳過了和j = 3(a)前兩個相互匹配的字符i = 3( b )、i = 4( c ),將從i = 5(a)開始匹配。
2):j = 3(a)和i = 5(a)相互匹配。

因此下一次匹配認爲j = 3(a)和i = 5(a)已經匹配過了,匹配從j = 4(b)和i = 6(b)開始,這樣的話也跳過了j = 3(a)這個字符的匹配。

同理可得第二次匹配也是沒必要的。

KMP算法

KMP算法匹配過程

利用KMP算法匹配的過程如下圖:
在這裏插入圖片描述

KMP算法的改進之處在於:能夠知道在匹配失敗後,有多少字符是不需要進行匹配可以直接跳過的,匹配失敗後,下一次匹配從什麼地方開始能夠有效的減少不必要的匹配過程。
在這裏插入圖片描述

在得到子串前綴和後綴的最長公共匹配字符數l後,以後在i = x,j = n處匹配失敗時,可以直接從i = x,j = l處繼續匹配(證明過程參考:嚴蔚敏的《數據結構》4.3章),這樣問題就很明顯了,我們要求出n和l對應的值,其中n是模式串字符數組的下標,l的有序集合通常稱之爲next數組,前面兩個模式串的next數組和下標n的對應如下:
在這裏插入圖片描述

模式串2完整匹配過程

有了這個next數組,那麼在匹配的過程中我們就能在j = n處匹配失敗後,根據next[n]的值進行偏移,其中next[0]固定爲-1,代表在當前i這個位置整個模式串和主串都無法匹配成功,要從下一個位置i = i + 1及j = 0處開始匹配,模式串2的匹配過程如下:
在這裏插入圖片描述

現在知道了next數組的作用,也知道在有next數組時的匹配過程,那麼剩下的問題就是如何通過代碼求出next數組及匹配過程了。

next數組的過程可以認爲是將模式串拆分成n個子串,分別對每個子串求前綴和後綴的最長公共匹配字符數l,這一點可以通過上圖(最長公共匹配字符數)看出來(沒有畫出l=0時的圖解)看出來。

代碼如下

next數組的代碼如下:

//改進前
void get_next(String T , int *next )
{
    int i = 1;
    int j = 0 ;
    next[1] = 0 ;
    while( i < T[0])
    {
        if( j == 0 || T[i] == T[j])
        {
            i++;
            j++;
            next[i] = j ;
        }
        else
        {
            j = next[j];
        }
    }

}

根據next數組求模式串在主串中的位置代碼如下:


//返回字串T在主串s第pos個位置
int Index_KMP (String S , String T , int  pos)
{
    int i = pos ;
    int j = 1;
    int next[225];
    get_next2(T , next);
    while( i <= S[0] && j <= T[0]){
        if(j == 0 ||  S[i] == T[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];
        }
    }
    if( j > T[0])
        {
            return i - T[0];
        }
        else
        {
            return 0;
        }


}

測試代碼如下:

int main()
{
    char str[255] = " ababaaaba";
    char str1[100] = " aab";
    int next[255];
    int i = 1 ;

    //字串和母串[0] 是用來保存字符串的長度方便使用 ;
    str[0] = 10;
    next[0] = 0;
    str1[0] = 3;
    get_next( str1 , next) ;
    printf("next 的值:\n");
    for(i = 1 ; i < 4 ; i++)
    {
        printf("%d  ", next[i]);
    }
    printf("\n-------------\n");
    printf("子串在母串第一次出現的位置爲\n%d\n",Index_KMP ( str , str1, 0));


}

在這裏插入圖片描述

KMP算法優化

再回過頭去看模式串2的next數組的圖:
在這裏插入圖片描述

如果模式串和主串的匹配在j = 6(b) 處失敗的話,根據j = next[6] = 1得知下一次匹配從 j = 1 處開始,j = 1處的字符和j = 6處的字符同爲c,因此這次匹配必定會失敗。
同樣的,模式串和主串的匹配在j = 7©處或在j = 9(b)處失敗的話,根據next數組偏移後下一次匹配也必定會失敗。

考慮如果模式串是: aaaac,根據一般的KMP算法求出的next數組及匹配過程如下:
在這裏插入圖片描述

顯而易見,在第二次匹配失敗後,第三、四、五次匹配都是沒有意義的,j = next[3]、j = next[2]、j = next[1]、j = next[0]這四處的字符都是a,在j = 3(a)處匹配失敗時,根據模式串本身就應該可以得出結論:可以跳過j = 2(a)、j = 1(a)、j = 0(a)的匹配,直接從i = i + 1 、j = 0處開始匹配.

所以優化過後的next數組應該是:

在這裏插入圖片描述

//改進後
void get_next2(String T , int *next )
{
    int i = 1;
    int j = 0 ;
    next[1] = 0 ;
    while( i < T[0])
    {
        if( j == 0 || T[i] == T[j])
        {
            i++;
            j++;
            if(T[i] != T[j] )
            {
               next[i] = j ;
            }
            else
            {  // aaaaaaabb  如果最後一個a不匹配,前面也不匹配
                next[i] = next [j] ;
            }
        }
        else
        {
            j = next[j];
        }
    }

}

完整代碼

#include <stdio.h>
typedef char *String;
//改進前
void get_next(String T , int *next )
{
    int i = 1;
    int j = 0 ;
    next[1] = 0 ;
    while( i < T[0])
    {
        if( j == 0 || T[i] == T[j])
        {
            i++;
            j++;
            next[i] = j ;
        }
        else
        {
            j = next[j];
        }
    }
}

//改進後
void get_next2(String T , int *next )
{
    int i = 1;
    int j = 0 ;
    next[1] = 0 ;
    while( i < T[0])
    {
        if( j == 0 || T[i] == T[j])
        {
            i++;
            j++;
            if(T[i] != T[j] )
            {
               next[i] = j ;
            }
            else
            {  // aaaaaaabb  如果最後一個a不匹配,前面也不匹配
                next[i] = next [j] ;

            }
        }
        else
        {
            j = next[j];
        }
    }

}

//返回字串T在主串s第pos個位置
int Index_KMP (String S , String T , int  pos)
{
    int i = pos ;
    int j = 1;
    int next[225];
    get_next2(T , next);
    while( i <= S[0] && j <= T[0]){

        if(j == 0 ||  S[i] == T[j])
        {
            i++;
            j++;
        }
        else
        {
            j = next[j];

        }

    }
    if( j > T[0])
        {
            return i - T[0];
        }
        else
        {
            return 0;
        }
}
int main()
{
    char str[255] = " ababaaaba";
    char str1[100] = " aab";
    int next[255];
    int i = 1 ;

    //字串和母串[0] 是用來保存字符串的長度方便使用 ;
    str[0] = 10;
    next[0] = 0;
    str1[0] = 3;
    get_next( str1 , next) ;
    printf("next 的值:\n");
    for(i = 1 ; i < 4 ; i++)
    {
        printf("%d  ", next[i]);
    }
    printf("\n-------------\n");
    printf("子串在母串第一次出現的位置爲\n%d\n",Index_KMP ( str , str1, 0));
	getchar(); 

}

後記

個人理解 理解 如果還不清楚請參考動態規劃之KMP字符匹配算法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章