KMP子串查找算法

KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,所以人們稱它爲克努特——莫里斯——普拉特操做(簡稱KMP算法,由他們的名字首字母組成)。html

KMP算法的關鍵是利用已經部分匹配的信息,儘可能減小模式串與主串的匹配次數以達到快速匹配的目的。算法

在介紹KMP以前先說一說樸素解法,也就是最簡單的暴力解法,樸素解法是採用窮舉的方式一個一個對比達到查找的功能,模式串與主串匹配失敗後又要從新從模式串的開頭繼續匹配,不考慮模式串已經判斷過的字符,因此效率差。數組

下面是樸素解法的實現代碼:數據結構

int sub_str_index(const char* s, const char* p)   // s是主串,p是模式串
{
    int ret  = -1;                    // ret記錄返回值,初始化爲-1表示沒有找到
    int sl = strlen(s);
    int pl = strlen(p);
    int len  = sl - pl;                 // len記錄主串的查找邊界,避免主串剩餘字符不足,提升效率

    for(int i=0; (ret<0) && (i<=len); i++)     // 若是沒有找到匹配的,且主串不會到邊界
    {
        bool equal = true;              // equal用於記錄臨時的匹配狀況,爲了下面的循環,默認爲真
        
        for(int j=0; equal && (j<pl); j++)     // 判斷當前位置主串是否與模式串徹底匹配 
            equal = (s[i + j] == p[j]);

        ret = (equal ? i : -1);           // 若是模式串所有匹配成功就返回匹配主串首字符的下標,不然返回-1表示失配
    }
    
    return ret;
}

 

接下來介紹KMP算法:函數

KMP算法是利用已知的信息減小無效匹配判斷的一種算法。spa

這是阮一峯的講解,我以爲很是好,供參考:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html3d

另外youtube上的黃浩傑也講的不錯,方便的朋友能夠去看看。指針

下面這張圖來自阮一峯博客,我作了一些修改,用它來舉例子:code

通過幾回查找後,這裏模式串的D和主串的空格失配了,爲了提升查找效率,咱們發現直接把模式串向後移動4位能夠更高效的匹配,由於主串當前的3個字符都不能和模式串匹配,從而省去了前面3次匹配的過程,實現效率的提高。htm

這個例子是要右移4位,4是怎麼來的呢,是經過模式串已經匹配的最後一個字符的位置(這裏就是模式串的第6個字符B,數組下標爲5)減去模式串最長匹配數2獲得的,那2又是怎麼來的呢,2是模式串模式串最多能匹配的字符個數,也就是部分匹配

首先咱們要先搞懂什麼是前綴後綴部分匹配以及部分匹配

拿上面的模式串"ABCDABD"來講

  前綴就是取從開頭到任意一個字符的子串;後綴則是取任意一個字符到末尾的子串;固然長度爲0或者爲模式串的總長度就沒有意義了,因此這兩個長度不計算

   

  下面這幅圖是在部分匹配成功後又匹配失敗的部分匹配計算演示圖;

  如今假設須要匹配第7個字符D(數組下標爲6),注意D左邊的B和A都匹配成功了,這時候直接使用最後一個匹配字符也就是前一個字符B(第6個字符)在前綴中的位置的部分匹配值;最後一個匹配字符是第6個字符B,它所對應的前綴位置經過它本身的部分匹配表記錄了(也就是2),這就是說這個字符B和模式串第2個字符(數組下標爲1)是對應的,第二個字符B的部分匹配值爲0,因此不能匹配的第7個字符的部分匹配值也是0;

最長匹配字符數就是最終的部分匹配值

 

  部分匹配就是某個後綴最多與前綴匹配的字符個數,上面這張圖能夠看到前綴是A開頭,因此後綴也得是A開頭才能匹配,這時候有ABD這個後綴能匹配,再貪婪一點,還能匹配更多嗎,發現最多隻能匹配2個字符,因此它的部分匹配就是2;

  部分匹配是記錄部分匹配的一個數據結構,由於咱們能匹配的模式串長度是不肯定的,因此須要針對每一個位置生成一個部分匹配

這裏就要用到KMP算法的部分匹配表了,部分匹配表用於記錄模式串模式串最多能匹配的字符個數;經過這個計數咱們就能知道移動的位數了,由於它記錄了當前字符最長匹配的字符個數,因此得出下面的公式:

移動位數 = 已匹配的字符數 - 對應的部分匹配值

使用計算公式咱們就能夠計算出右移的位數,已匹配字符數爲6(已匹配ABCDAB),最後一個匹配的字符B的部分匹配表爲2,因此右移6-2=4位.

網上不少人把這個部分匹配表寫成next數組,我這裏根據老師的代碼寫成了int類型的指針,用堆空間記錄,長度與模式串長度一致,每一個單位爲int類型,等效於int數組。

int* make_pmt(const char* p)·                     // 部分匹配表生成函數,給定模式串指針做爲參數,返回int*記錄部分匹配表,可充當數組使用,長度爲模式串長度;參數合法性由調用者判斷
{
    int len = strlen(p);
    int* ret = static_cast<int*>(malloc(sizeof(int) * len));  // 申請堆空間用於記錄部分匹配表,使用者須要釋放

    if( ret != NULL )                         // 只有堆空間申請成功才能操做
    {
        int ll = 0;                           // ll==>longest length,最長部分匹配值,初始化最長部分匹配值爲0

        ret[0] = 0;                          // 第一個元素沒有匹配的因此直接寫爲0

        for(int i=1; i<len; i++)                  // 從第二個元素開始遍歷
        {
            while( (ll > 0) && (p[ll] != p[i]) )          // 若是已經有匹配過的字符,可是下一個字符不匹配
            {
                ll = ret[ll-1];                   // 把ll重置爲模式串的第ll個字符的部分匹配值,數組下標從0開始因此要減1,p[11-1]的部分匹配值存儲於ret[ll-1];
            }

            if( p[ll] == p[i] )                  // 每匹配成功一個字符ll就遞增
            {
                ll++;
            }

            ret[i] = ll;                      // 進入下一輪循環前要把ll值保存進部分匹配表,即ret[i]記錄p[i]的最長部分匹配表
        }
    }

    return ret;
}

下面是kmp函數:

int String::kmp(const char* s, const char* p)      // s主串,p模式串
{
    int ret = -1;
    int sl = strlen(s);
    int pl = strlen(p);
    int* pmt = make_pmt(p);                // 獲取子串的部分匹配表,注意該函數返回的是堆空間,用完須要釋放

    if( (pmt != NULL) && (0 < pl) && (pl <= sl) )  // 只有當部分匹配表獲取成功、模式串長度大於0且不超過源串長度的時候才須要計算
    {
        for(int i=0, j=0; i<sl; i++)          // for初始化i和j變量,i用於遍歷每一個主串的字符,j用於記錄已匹配的模式串字符數
        {

            while( (j > 0) && (s[i] != p[j]) )    // 有已匹配字符可是後續字符又匹配失敗時
            {
                j = pmt[j-1];              // 移動模式串,使模式串第一個字符對齊到主串下一個匹配的字符位置,這是一次性移動,再也不是暴力搜索中的每次只移動一次
            }

            if( s[i] == p[j] )             // 若是匹配成功,記錄匹配模式串字符個數的j加1
            {
                j++;
            }

            if( j == pl )                // 若是當前已匹配的模式串字數與模式串長度相等說明匹配成功
            {
                ret = i + 1 - pl;           // 返回匹配源串的第一個字符的下標並跳出循環結束尋找;此時i指向的是匹配的主串的最後一個字符,減去子串長度後等於第一個匹配的字符的前一個,因此要加1日後挪一個
                break;
            }
        }
    }

    free(pmt);                      // 記得釋放部分匹配值生成函數申請的堆空間

    return ret;
}
相關文章
相關標籤/搜索