KMP算法及其改進


KMP算法及其改進

字符串匹配算法也就是從一個很長的字符串里面找出與我們手中的字符串相匹配的字符串(是這個大字符串的第幾個字符開始),對於這個問題我們有很簡單的解法,叫BF算法,Brute Force也就是蠻力的意思,充分依靠計算能力來解決問題的方法,對於這種解法可以用下面的圖片來表述:

 

上面的算法就是BF算法,不好之處是效率太低了,因為就像第三趟比較中那樣,我們只有最后一個元素沒有匹配上就要從頭再來,主串的對應元素竟然要回頭從第四個元素開始比較,我們明明比較到了主串的第七個元素,前面的工作全部白費了。

KMP算法的想法減少我們比較時主串又回溯回去的這種現象,在KMP算法里,比較過的主串的元素(匹配成功)不會再進行比較,我們只是在失敗的時候選中字串的某一個元素來和主串所對應的元素進行比較。最核心的就是next數組了,next數組對於比較失敗時的情況給出了明確指引下一步我們應該拿子串的那一個元素再進行比較,這種指引是建立在對字串充分解析的基礎之上的。

上面就是一個字串,下面的一行就是我們給出的next數組,用法也很簡單,譬如說我們子串的第七個元素a與主串對應的元素進相比較失敗了,由這一點可以說明,子串前面的六個元素都與主串匹配成功了,我們下一步就依照next數組的指示,用子串的第四個元素來與主串當中剛才沒有匹配成功的那個元素比較,這樣選擇的依據是,我們把第四個元素與主串中剛才失敗的那個元素對齊后,子串的第四個元素前面的元素都是匹配的。

next數組的產生,對於第一個元素a來說,next[1]永遠都是0,意味着我們要用第0號元素與主串元素對其,但沒有0號元素,也就是說我們要用1號元素與主串的下一個元素對齊,對於第二個元素b來說,我們假想他與主串匹配失敗,用子串的第一個元素來對齊,這種情況直到第五個元素發生了變化,我們用第二個元素來對齊,可以看見子串的第一個元素a與主串的失敗元素的前一個元素是完全匹配的都是a,這樣相比較BF算法我們就少比較了一次,第六個元素C如果失敗了,我們就用第三個元素C來與主串中的元素對齊,發現前面的ab兩個元素不用比較了,再說子串第八個元素失敗的情況,我們用子串的第五個元素與主串元素對齊,可以發現前面的abca四個元素是完全匹配的。

通過充分解析字串的內容,我們可以給出next數組來指引我們加快匹配過程,具體的怎么填寫next數組有具體的程序,但這里還是要說一下我的思路,假設第一個元素a比較失敗,沒辦法,我們只能將子串后移一下與主串在進行比較,填寫0意味着我們放棄與主串的這個元素比較轉而與主串的下一個元素進行比較,如果第二個元素b比較失敗,我們發現它的前面只有一個a,我們就填1了,這里說的太牽強了,那第六個元素來說,當它失敗的時候,我們發現子串中他前面的元素中 后綴ab(4,5)與前綴ab(1,2)是一樣的,我們如果把第三個元素移到此處對齊時就會發現前兩個元素已經完全匹配了,這就是基於我們對子串中失敗元素前面(所有元素成為一個整體)的前綴和后綴相不相同分析而來的,因為我們如果依照next數組移動了,肯定就是原來子串中失敗元素的前綴和后綴對齊在一起。前綴后綴都是來年各個元素,這里我們+1,因為對齊的話主串中失敗元素正好對的是子串的第三個元素。

分析失敗元素的前綴和后綴得到了next數組,前綴為從前開始的前幾個元素的組合,后綴是左側緊貼失敗元素的幾個元素的組合,例如     next[8]==5,是因為1234和4567都是abca,4+1==5;

當然這種方法還有值得改進的地方,如果我們按照next數組的指示換子串的另一個元素來比較,但這個元素與原來的子串中的那個元素是相等的,這樣和主串比較不還是失敗么!!!如子串中第六個元素是C,它比較失敗了,我們按照next數組指示選擇第三個元素來接他的班,但第三個元素與第六個元素相等都是C,不用想,依舊會比較失敗,這里我們怎么辦???我們要盡享以下判斷,如果第六個元素與第三個元素不相等我們就用第三個元素,如果相等了,我們就用第三個元素對應的next成員來接替(也就是第一個元素a)

假如上面的不好理解的話,我們可以假設我們傻啦吧唧的用了第三個元素C來對齊比較,結果不出所料,失敗了,我們又會用第三個元素對應的next元素來與之對其進行比較——————我們上面做的只不過是提前了一步,投石問路,少走了一步彎路而已。

因為next數組是從前往后依次建立的,上面的做法最終使得第i個元素與它對應的next[i]總不會是相同的,這樣避免了可預料的重復失敗,進一步加快了匹配速度,按照這種思想建立如下:

注意上面的next數組,其實少了一個元素,我們沒有看見next[0]但是在實際的next數組中這個元素肯定是存在的,這樣的話我們的next數組要比子串長度大一,因為next[0]永遠都不會被用到,所以我們也就不對他進行賦值了,但他還是存在的。

下面貼一下改進后的kmp_next數組產生程序:

void get_next(HString S, int next[])
{
    int i = 1, j = 0;
    HString subs1, subs2;
    next[1] = 0;//這是肯定的,next[0]我們沒有處理
    while (i < S.StrLength())//如果后綴i還沒有到達末尾
    {
        S.SubString(subs1, i, 1);//這就是取S的第i個字符的意思
        S.SubString(subs2, j, 1);
        //因為j等於0,所以就不會返回什么東西了
        /* 下面的處理函數當進入if的時候也就是說前綴和后綴字符相等,通常情況下我們進行的操作就是 i++,j++,next[i]=j;意味着當我們在后綴i這個地方發生不匹配的時候,我們可以使用第j個元素進行匹配,但是呢我們沒有考慮到的一個問題就是,如果當前的(增加后的)后綴i(潛在發生不匹配的元素)與要接替他的元素(增加后的J)是相等的該怎么辦呢?如果相等,也就是說還是會不匹配,假設我們就真的寫了 next[i]=j;因為第i個元素與要接替他的第j個元素是相等的,我們很快就會用第j個元素所對應的next元素來接替第j個元素,所以呢,我們提前考慮一步,如果當前的i與要接替他的元素j是相等的,那我們就跨過一步,直接讓要接替j的元素來接替i,如果不想等,那么我們可以放心的接替也就是直接讓 next[i]=j;

        */
//看看上面取到的字符相不相等,至於j==0這是實際真的會發生的,此時進入if的原因就不//是后面的相等了,而是0==j這一條件。
        
        if (0 == j || subs1.StrCompare(subs2) == 0)
        {
            ++i;
            ++j;
            S.SubString(subs1, i, 1);
            S.SubString(subs2, j, 1);

            if (subs1.StrCompare(subs2) != 0)
                next[i] = j;
            else next[i] = next[j];
        }
        else //發生不匹配,那我們就用j所對應的next元素來接替他接着進行比較
        {
            j = next[j];
        }

    }
}

 

 

 我們按照上面給出的S再推一遍,首先i指向a,j==0指向第0個元素(S沒有第0號元素),接下來就是next[1]==0;這是永遠都成立的,也就是說要放棄與主串中的當前比較失敗元素的繼續比較轉而與主串的下一個元素進行比較,然后我們看到了一個if條件問相不相等,怎么會相等?S都沒有第0號元素我們怎么取它?所以subs2什么也沒有取回來,好在if在j==0的時候也可以進入,我們將i和j都加加,i現在是2,j現在是1,再取出對應的第X個字符,兩者不相等,我們應該next【i】=j;這也就是普通的KMP算法的做法,甚至都不比較相不相等,如果兩者相等的話我們就執行next【i】=next【j】這里有點遞歸的意思,因為上面已經說明了為什么這樣所以就不再解釋了,這樣next【2】=1了,然后再次看看能不能進入if,結果進不去了,這個時候就執行else的 j=next[j]了,j也就變成了next【1】也就是0而i卻沒有發生變化i還是2,j變成了0,在試試if果斷進去了因為j==0,然后加加,i是3,j是1,取字符,結果不相等,next【i】=next【j】,next[3]==1,然后出去發現依然不相等,j=next[j],j變成了0,進入大if,i為4,j是1,二者相等,相等的話next【i】=next【j】也就是0,然后出去,在進入if,i是5,j是2,也相等next【i】=next【j】這樣next【5】=1,又進入if,i為6,j是3,相等,next【6】=1,又進入,i為7,j是4,又相等,next【7】=0,再一次進入,i為8,j是5,不相等,next【8】=j也就是5,然后進不去了,j=next【j】也就是1,i為8,j是1,不相等,j變成了next[J]==0,成功進入,i為9,j是1,相等,next【9】=next【1】==0然后判斷  i < S.StrLength() 此時是相等的,所以跳出while,next數組產生完畢。

然后下面再貼一下怎么使用next數組:

//S是模式串,T是主串,pos是說在T中第幾個元素之后開始搜索,next就是我們要傳入的next數組
//其中S非空,POS大於等於1,小於主串的長度
int Index_KMP(HString T, HString S, int pos, int next[])
{
    int i = pos,j = 1;//
    HString subs1, subs2;
    while (i <= T.StrLength() && j <= S.StrLength())//如果主串和子串都沒有比較完畢,我們還需要繼續比較。
    {
        T.SubString(subs1, i, 1);//取主串的第i個字符
        S.SubString(subs2, j, 1);//取子串的第j個字符
        if (j == 0 || subs1.StrCompare(subs2) == 0)
        {
            ++i; ++j;//相等的話就都加1,在比較下一對字符,j==0的進入條件是為了下面else產生的j=next[j]==0,這樣i增加了j卻沒有增加

        }
        else
            j = next[j];//再從子串的第next[j]個元素開始比較,也就是當j是0的時候,我們放棄與主串的當前元素比較,轉而與主串下一個元素進行比較,所以
//上面有i++,j++,i加加意味着主串比較元素后移一位,而j++卻變成了1,我們要用子串的第一個元素和主串元素比較,這不就是開始新一輪的比較么?
} if (j > S.StrLength()) { return i - S.StrLength();//就是說從主串的這個元素開始我們發現了匹配的子串 } else return 0; }

 

 我們使用的時候,要制定從主串的第幾個元素開始進行尋找,也就是設置的pos值,T和S分別是主串和子串,next是我們根據子串T已經產生好的next數組,實際上穿的是數組的名字,也就是地址。void get_next(HString S, int next[])是得到next數組的函數,我們傳進去子串S以及我們自己動態分配好了的next數組,例如可以這樣寫 char*p=new char[S.StrLength()+1],我們傳進去的實際上就是p,至於為什么p數組的長度比S的長度大1,上面已經解釋過了就不再說了。

順便說一句KMP算法的時間復雜度是:O(m+n)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM