KMP算法詳解

KMP算法是解決字符串匹配的常用算法之一,也就是在主串(比如aabbccdd)中的子串(bc)定位問題。子串稱為P,如果它在一個主串稱為T中出現(xiàn),就返回它的具體位置,我們先來看看普通的字符串匹配是怎么做的

最基礎(chǔ)的匹配

思路:從左到右一個個匹配,如果這個過程中有某個字符不匹配,將子串向右移動一位,繼續(xù)從左到右一一匹配。

當(dāng)匹配到如圖第四個字符位置后,匹配失敗,子串后移,繼續(xù)匹配


image

第一位匹配失敗,繼續(xù)后移...


image

image

直到匹配成功

[圖片上傳失敗...(image-bdd392-1548667452278)]
代碼如下:

public class Normal {
    
    public static void main(String[] args) {
        int index = bf("ABCABCEFG", "ABCE");
        System.out.println(index);
    }
    
    public static int bf(String ts, String ps) {
        char[] t = ts.toCharArray();
        char[] p = ps.toCharArray();
        int i = 0; // 主串的位置
        int j = 0; // 子串的位置

        while (i < t.length && j < p.length) {
            if (t[i] == p[j]) { // 當(dāng)兩個字符相同,就比較下一個
                i++;
                j++;
            } else {
                i = i - j + 1; // 一旦不匹配,i后退
                j = 0; // j歸0
            }
        }

        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }
}

這種方式是效率最低,匹配次數(shù)最多的情況,接下來看KMP的解決思路

KMP中的PMT

KMP在遇到下圖位置時,不會很無腦的把子串的j移動到第0位,主串的i移動到第1位,然后進(jìn)行T[i]==P[j]的比較
[圖片上傳失敗...(image-1dbb87-1548667452278)]
因?yàn)閺膱D上可以看得出后移一位后子串前三位(ABC)和主串的T[1-4](BCA)肯定是不匹配的,無需白白浪費(fèi)這幾次比較,最好應(yīng)該是直接讓i不變,j==0,如下圖

image

從這里開始匹配,省去了前面的幾次無用匹配。

KMP思想:利用前面匹配的信息,保持i指針不變,通過修改j指針,讓子串盡量地移動到有效的位置。

整個KMP的重點(diǎn)就在于當(dāng)某一個字符與主串不匹配時,我們應(yīng)該知道j指針要移動到哪?

先用肉眼來看一下規(guī)律:

image

如圖:C和D不匹配了,我們要把j移動到哪?顯然是第1位。為什么?因?yàn)榍懊嬗幸粋€A相同可以用:

image

再看一種:

image

可以把j指針移動到第2位,因?yàn)榍懊嬗袃蓚€字母是一樣的:

image

我們可以看出來,匹配失敗的時候,j變?yōu)閗,j前面的的n個字符等于子串開頭到k位置的n個字符的值

image

即:P[0 ~ k-1] == P[j-k ~ j-1]

這時我們發(fā)現(xiàn)規(guī)律了,其實(shí)就是要求當(dāng)前j之前的字符串也就是ABCAB它的首尾對稱的長度最大長度也就是PMT值。

PMT中的值是字符串的前綴集合與后綴集合的交集中最長元素的長度。

例如,對于”aba”,它的前綴集合為{”a”, ”ab”},后綴集合為{”ba”, ”a”}。
兩個集合的交集為{”a”},
那么長度最長的元素就是字符串”a”了,長度為1,所以對于”aba”而言,它在PMT表中對應(yīng)的值就是1。
再比如,對于字符串”ababa”,它的前綴集合為{”a”, ”ab”, ”aba”, ”abab”},
它的后綴集合為{”baba”, ”aba”, ”ba”, ”a”}, 
兩個集合的交集為{”a”, ”aba”},其中最長的元素為”aba”,長度為3。

所以上面最后一個圖的情況下,j位置之前的字符串的PMT值為2,所以j的值變成2。

KMP之next數(shù)組

那么好了接下來核心就是求得P串每個下標(biāo)元素對應(yīng)的k值即可,因?yàn)樵赑的每一個位置都可能發(fā)生不匹配,我們要計算每一個位置j對應(yīng)的k,所以用一個數(shù)組next來保存,next[j] = k,表示當(dāng)T[i] != P[j]時,j應(yīng)該變?yōu)閗。

求next數(shù)組代碼如下

public class Next {
    
    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
            if (k == -1 || p[j] == p[k]) {
                next[++j] = ++k;
            } else {
                k = next[k];
            }
        }
        return next;
    }
}

通過上面代碼可以直接算出j為0和1時的k,當(dāng)j為0時,已經(jīng)無法后退了所以設(shè)置為-1初始化值,當(dāng)j為1時,它的前面只有下標(biāo)0了,所以next[0]=-1,next[1]=0.

接下來就是兩種主要情況了

if (k == -1 || p[j] == p[k]) {   第一種p[j] == p[k]
    next[++j] = ++k;
} else {                         第二種p[j] != p[k]
    k = next[k];
}

第一種p[j] == p[k]

image

p[j] == p[k]時,有next[++j] = ++k;
因?yàn)楫?dāng)在p[j-1]處匹配失敗后,j-1變?yōu)閗-1,從k-1處重新開始匹配,原因就是他們共同有一個前綴A,所以當(dāng)p[j] == p[k]后,他們就擁有了前綴AB所以k++;

第二種p[j] != p[k]

image

此時代碼是:k = next[k];原因看下圖

image

像上邊的例子,我們已經(jīng)不可能找到[ A,B,A,B ]這個最長的后綴串了,但我們還是可能找到[ A,B ]、[ B ]這樣的前綴串的。所以這個過程就像在定位[ A,B,A,C ]這個串,當(dāng)C和主串不一樣了(也就是k位置不一樣了),那當(dāng)然是把指針移動到next[k]。

有了next數(shù)組之后就一切好辦了,我們可以動手寫KMP算法了:

public class Kmp {
    public static int KMP(String ts, String ps) {
        char[] t = ts.toCharArray();
        char[] p = ps.toCharArray();
        int i = 0; // 主串的位置
        int j = 0; // 模式串的位置
        int[] next = getNext(ps);

        while (i < t.length && j < p.length) {
            if (j == -1 || t[i] == p[j]) { // 當(dāng)j為-1時,要移動的是i,當(dāng)然j也要?dú)w0
                i++;
                j++;
            } else {
                // i不需要回溯了
                // i = i - j + 1;
                j = next[j]; // j回到指定位置
            }
        }

        if (j == p.length) {
            return i - j;
        } else {
            return -1;
        }
    }
}

KMP改進(jìn)

KMP算法是存在缺陷的,來看一個例子:比如主串是aaaabcde,子串是aaaaax,next值為012345,當(dāng)i=5時,如下圖:

image

我們發(fā)現(xiàn),當(dāng)中的②③④⑤步驟,其實(shí)是多余的判斷。由于子串的第二、三、四、五位置的字符都與首位的“a”相等,那么可以用首位next[1]的值去取代與它相等的字符后續(xù)next[j]的值,這是個很好的辦法。因此我們對求next函數(shù)進(jìn)行了改良。

public class Next2 {
    public static int[] getNext(String ps) {
        char[] p = ps.toCharArray();
        int[] next = new int[p.length];
        next[0] = -1;
        int j = 0;
        int k = -1;
        while (j < p.length - 1) {
            if (k == -1 || p[j] == p[k]) {
                if (p[++j] == p[++k]) { // 當(dāng)兩個字符相等時要跳過
                    next[j] = next[k];
                } else {
                    next[j] = k;
                }
            } else {
                k = next[k];
            }
        }
        return next;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 字符串匹配KMP算法詳解 1. 引言 以前看過很多次KMP算法,一直覺得很有用,但都沒有搞明白,一方面是網(wǎng)上很少有...
    張晨輝Allen閱讀 2,629評論 0 3
  • 概述 KMP是字符串匹配的經(jīng)典算法。其中包含的思想,是非常有趣的。本文作為KMP算法的介紹和備忘錄。 場景 KMP...
    oceanLong閱讀 1,760評論 1 1
  • 在數(shù)據(jù)結(jié)構(gòu)課上老師講了kmp算法,但當(dāng)時并沒太懂,現(xiàn)在把思路重新理一遍。 1.kmp算法簡介 KMP是三位大牛:D...
    zealscott閱讀 291評論 0 1
  • 原鏈接:KMP算法詳解|CloudWong 傳統(tǒng)的字符串匹配模式(暴力循環(huán)) 子串的定位操作通常稱作串的串的匹配模...
    簡Cloud閱讀 4,029評論 1 22
  • 詳解:https://www.cnblogs.com/yjiyjige/p/3263858.htmlhttps:/...
    小幸運(yùn)Q閱讀 437評論 0 1

友情鏈接更多精彩內(nèi)容