1. 程式人生 > 其它 >字串匹配演算法KMP詳細解釋

字串匹配演算法KMP詳細解釋

技術標籤:資料結構與演算法字串演算法

1、蠻力匹配法

問題很簡單,當然也有最直接、最直觀也是最好想到的方法,蠻力串匹配。即兩個字串像物流傳送帶一般,主串固定,子串一步步像前移動,一位位匹配比較,直到完全匹配找到想要的結果的位置。效果即如下圖所示,將T長度為m的n-m+1個子串逐一和P進行比對,發現完全每一位匹配的位置即我們需要的結果。圖中P字串上黑色表示該位已成功匹配,而綠色表示當前匹配未成功的位置,白色表示未匹配字元位置。

在這裡插入圖片描述

int matchString(char * T, char * P, int lenT, int lenP)
{
    int i=0;
    int j=
0; while(i<(lenT-lenP+1))//i最大的位置只能取到lenT-lenP { if(T[i+j] == P[j])//當子串匹配就一直找下去 { if(lenP-1 == j)//整個子串都匹配 { return i; } else { j++; } } else//如果不匹配則i的位置+1,j的位置從頭開始
{ i++; j=0; } } }

上面程式很好理解,就是從位置0開始,先固定當前主串的位置i,然後一一比較主串下面m個字元是否和P完全匹配,匹配則輸出主串中位置i。一旦這個m位比較過程中,如果發生當前字元不匹配的情況,則主串i的位置相比較這次匹配開始位置增加1,準備進行下一次比較,而j自然復位到字串P的首字元地方。一般情況下,我們知道m遠小於n的,這樣蠻力匹配演算法的總體時間複雜度為O(n×m)。當然,蠻力匹配演算法的寫法有很多,也不一定就是上面的形式,但是隻要不發生質的改變,所有的蠻力匹配演算法寫法時間複雜度是不會發生改觀的。

但是,蠻力演算法明顯時間複雜度過高,不適合規模稍微大一些的應用環境,因此就需要改進。這裡我們觀察不難發現,蠻力演算法之所以需要大量的時間,是因為存在大量的區域性匹配,而且每次匹配一旦失配,主串和模式串的字元指標都需要回退,並從頭開始下一輪的嘗試。實際上,我們在整個過程中重複了很多操作,因為在完全成功匹配之前,我們曾經很大可能匹配成功過很多次部分字元。只要充分利用這些資訊,就可以不需要讓主串完全回退到上次開始比較的下一個字元,模式串一樣的道理,這樣就可以大大提高匹配演算法的效率。下面我們就來看看關於這個問題改進的演算法KMP的原理。

2、KMP演算法

KMP演算法是根據三位發明者 Knuth、Morris 和 Pratt 名字的首字母命名的。在介紹之前,我們詳細看看下面這張圖:
在這裡插入圖片描述
當第一輪對比進行到最後一對字元的時候,由於’a’和’b’發生失配,如果是蠻力演算法將會讓這兩個字元指標回退(即主串i = i-j+1,和模式串j=0),然後又從頭一一對比。然而事實上,指標i完全不必要回退,通過第一輪比對我們清楚的知道,主串T的子串substr(T,i-j,j)的第三位和第四位其實和模式串P前兩位是完全匹配的,並且模式串P第一位也並不等於主串T第二位,所以可以直接將模式串P直接移到主串第三位對齊,並且前兩位不需要比較,直接從第三位開始繼續比較即可。

那麼一般性的問題就來了,模式串P在任何上次失配情況下,應該右移幾個單元,並且從第幾位開始比較呢?這就是KMP中next表應該完成的工作了。

next表理解

一開始我就要強調一下,next表都是基於模式串,即子串來建立的。next表決定了當兩個字串一個個字元匹配的時候出現失配,應該回退到哪,即失配回退是根據失配那一位的next值所決定。回退的規則簡單來說也就是一句話:返回失配位之前最長公共前後綴對應的字首後一位的地方。怎麼理解這句話呢?先看下圖:
在這裡插入圖片描述
上面的話需要分為“最長公共前後綴”和“字首後一位”兩部分來理解。當P和T匹配遇到”c”發生失配,那麼現在P應該從“c”回退到第幾個字元,完全由“c”前面子字串“beabe”所決定。我們看到這個字串是關於“a”前後對稱,也就是說最長的公共前後綴就是“be”,那麼下一次比較的開端就是字首“be”後一位“a”。下一次匹配移動如下:

在這裡插入圖片描述
next表如何建立

next表的每一位反映該位失配後回退的地方,它是由前一位字元在整個前子字串中最長公共前後綴的長度值所決定(說的有些繞口。。),我們還是直接看下面的總結:
①、第一位字元的next值設定為-1,因為當第一位就開始失配,直接將模式串下移一位即可,無需多說。同樣道理,第二位也一樣,其前子字串僅一個字元,所以next值即為0。
②、後面的,當某位前一位字元的前一個字元對稱程度為0的時候,只要將該位前一位字元與子串第一個字元進行比較即可。例如abcdae,因為“d”字元與前面無對稱項,所以只需要比較a和開頭字元比較即可。
③、以此推理,如果某位前一位字元的next值是1,即該位前一位字元的前一個字元與開頭字元相等,那麼我們就把該位前一位字元與子串第二個字元進行比較,如果也相等,說明對稱程度就是2了,即該位的next值為2。
④、當然如果一直相等,就一直一位位累加繼承。但是絕大多數不可能會如此順利對稱下去,如果遇到下一個不相等了,那麼說明不能繼承前面的對稱性了。這種情況只能說明沒有那麼多對稱了,但是不能說明一點對稱性都沒有,所以遇到這種情況就要重新來考慮,這個也是難點所在。
④、一旦發生不能累加繼承,則需要在對稱的前後綴字串中繼續尋找子對稱。如下圖所示:
在這裡插入圖片描述
“abadabab… …”中“b”不能繼續繼承前面的對稱序“aba”,所以下一步做的在對稱序中繼續找次對稱序,最後發現子對稱“ab”。如果未能成功尋找到則b後一位的next值為0。


void cal_next(char * str, int * next, int len)
{
    int i,j;
    next[0] = -1;
    for(i=1; i<len; i++)
    {
        j = next[i-1];
        while(str[j+1] != str[i] && j>0)
        {
            j = next[j];
        }

        if(str[i] == str[j+1])
        {
            next[i] = j+1;
        }
        else
        {
            next[i] = -1;
        }
    }
}

int KMP(char * str, int slen, char * ptr, int plen, int * next)
{
    int s_i=0;
    int p_i=0;

    while(s_i < slen && p_i < plen)
    {
        if(str[s_i] == ptr[p_i])
        {
            s_i++;
            p_i++;
        }
        else
        {
            if(p_i == 0)
            {
                s_i++;
            }
            else
            {
                p_i = next[p_i] + 1;
            }
        }
    }
    return (p_i == plen) ? (s_i-plen):-1;
}