串的兩種模式匹配方式(BF/KMP算法)

前言

串,又稱做字符串,它是由0個或者多個字符所組成的有限序列,串一樣能夠採用順序存儲和鏈式存儲兩種方式進行存儲,在主串中查找定位子串問題(模式匹配)是串中最重要的操做之一,而不一樣的算法實現有着不一樣的效率,咱們今天就來對比學習串的兩種模式匹配方式:c++

  • 樸素的模式匹配算法(Brute-Force算法,簡稱BF算法)算法

  • KMP模式匹配算法數組

樸素的模式匹配算法(BF算法)

BF算法是模式匹配中的一種常規算法,它的思想就是:微信

  • 第一輪:子串中的第一個字符與主串中的第一個字符進行比較
    • 若相等,則繼續比較主串與子串的第二個字符
    • 若不相等,進行第二輪比較
  • 第二輪:子串中的第一個字符與主串中第二個字符進行比較......
  • 第N輪:依次比較下去,直到所有匹配

圖示說明:

第一輪:學習

第二輪:優化

...... 原理一致,省略中間步驟設計

第五輪:3d

第六輪:指針

代碼實現:

看完文字與圖例講解,咱們來動手實現一個這樣的算法code

簡單概括上面的步驟就是:

主串的每個字符與子串的開頭進行匹配,匹配成功則比較子串與主串的下一位是否匹配,匹配失敗則比較子串與主串的下一位,很顯然,咱們可使用兩個指針來分別指向主串和子串的某個字符,來實現這樣一種算法

匹配成功,返回子串在主串中第一次出現的位置,匹配失敗返回 -1,子串是空串返回 0

int String::bfFind(const String &s, int pos) const {
    //主串和子串的指針,i主串,j子串
    int i, j;
    //主串比子串小,匹配失敗,curLenght爲串的長度
    if (curLength < s.curLenght)
        return -1;
    
    while (i < curLength && j < s.curLength) {
        //對應字符相等,指針後移
        if (data[i] == s.data[j])
            i+, j++;
        else {  //對應字符不相等
            i = i -j + 1;   //主串指針移動
            j = 0; //子串從頭開始
        }
        //返回子串在主串的位置
        if (j >= s.curLength) 
            return (i - s.curLength);
        else return -1;    
    }
}

注:代碼只爲體現算法思路,具體定義未給出

這種算法簡單易懂,卻存在着一個很大的缺點,那就是須要屢次回溯,效率低下,若主串爲 000000000001 子串爲00001,這就意味着每一輪都要比較到子串的最後一個字符纔會匹配失敗,有沒有更好的辦法呢?下面的KMP模式匹配算法就很好的解決了這一問題

KMP模式匹配算法

若是僅僅進行一些少許數據的運算,可能你甚至以爲BF算法也還行,起碼是很容易寫出來的,畢竟能跑的就是好程序,可是一旦數據量增大,你就會發現有一些 「無用功」 真的會大大的拖慢你的速度

KMP模式配算法是由 D.E.Knuth,J.H.Morris,V.R.Pratt 三位前輩提出的,它是一種對樸素模式匹配算法的改進,核心就是利用匹配失敗後的信息,儘可能減小子主串的匹配次數,其體現就是 主串指針一直日後移動,子串指針回溯

圖示說明:

下面所表示的是樸素模式匹配算法的過程,咱們看看若是使用KMP算法的思想,哪些步驟是能夠省略掉的

① 中前五個元素,均互相匹配,知道第六個元素才匹配失敗,按照BF算法來講,就直接進行 ② ③ 操做,可是,咱們能夠發現,子串中的前三個元素 a b c 均不是相同的,可是在 ① 中已經與 主串相匹配,因此 子串分別與主串中的第二 第三個元素匹配 必定是不匹配的,因此圖中的 ② ③ 都可以省略

在 ① 中 子串中的 第一第二個元素 ab 和第四第五個元素 ab 是相同的,且 第四第五個元素 ab 已經與主串中的 第四第五個元素匹配成功,這意味着,子串中第一第二個元素 ab 必定與 主串中 第四第五個元素相匹配,因此 ④ ⑤ 步驟能夠省略

若是按照這種思路,上面的例子只須要執行 ① 和 ⑥ 就能夠了

next 數組值推導

(一) 主串指針是否須要回溯

咱們觀察上面的兩種過程 ,BF算法-①②③④⑤⑥,KMP算法-①⑥,若是咱們如今假定有兩個指針,i 和 j,分別指向主串和子串中的所處位置,從上圖咱們能夠知道,主串指針,也就是 i 的值在 ① 的狀態下, 指針指向6的位置,而在 ②③④⑤ 中卻分別指向了2345,而在 ⑥ 中仍指向6的位置

這說明,樸素模式匹配算法,主串的 i 值會不斷的進行回溯,可是 KMP模式匹配算法將這種不必的回溯省略掉了,因此減小了執行次數

(二) 子串指針優化總結

既然主串指針不進行回溯,那麼還能夠優化的就是 子串指針了,通常會遇到兩種狀況 咱們舉兩個例子:

  • 若是子串爲 abcdef,主串爲abcdexabcdef,當第一輪匹配到第六個字符f和x的時候,匹配失敗了,這個時候若是按照樸素模式匹配,就須要拿子串的首元素a去分別和主串的bcde進行比較,可是因爲子串f元素前的元素中沒有相同的元素,而且與主串匹配,因此a與主串中的2-5號元素 即 bcde 都是不可能相匹配的,全部這幾部均可以省略,直接讓a和主串中的x去匹配

  • 若是子串爲abcabx,主串爲abcababcax,在第一輪中,前五個元素子主串分別相匹配,第六個元素位置出錯,按照樸素模式匹配,咱們須要拿子串首元素a,依次與主串中的a後面的元素匹配,可是子串前面三個字符abc是不相等的,按照咱們第一種狀況的經驗,就直接跳過這些步驟了,全部咱們直接拿 子串a與 主串第四個元素a進行比較就能夠了,可是咱們發現,子串中出錯的位置x前的串 abcab 的前綴和後綴都是 ab,既然第一輪的時候,已經匹配成功,那就意味着,子串中的 第一第二個元素ab必定與 主串中 第四第五個元素 ab相等,因此這個步驟也能夠省略,也就直接能夠拿子串前綴ab後面的c開始於a進行比對,這也就是咱們上面圖中例子的詳細思路

總結:因此咱們得出規律,子串指針的值取決於,子串先後綴元素的類似程度

想要應用到具體代碼中,咱們能夠把子串位置變化 的 j 值定義成一個next數組,且長度與子串長度相同

$$next[j]=
\begin{cases}
-1 && 當j = 0\
max & {k|0<k<j 且 "T_0 T_1...T_k-_1" = "T_j-_k T_j-_k+_1 ...T_j-_1"} & 當集合不爲空時\
0 &&其餘狀況 \
\end{cases}$$

  • 狀況1:當 j = 0 時,next[j] = -1, 表示子串指針指向下標爲0的元素的時候匹配失敗,子串沒法回溯,(j不能賦值-1) ,此時將主串指針後移一位,子串不,進行下一輪比較
  • 狀況2:在已經匹配的子串中,存在相同的前綴串 T0 T1 ... Tk-1 和後綴串 Tj-k Tj-k+1 ... Tj-1,子串指針則回溯到next[j] = k的位置,而後進行下一趟比較,例如:子串 abcabc 有相同前綴和後綴ab 因此子串指針回溯到 c的位置

  • 狀況3:在已經匹配的子串,若不存在相等的前綴和後綴,則主串指針不動,子串指針回溯到 j = 0 的位置,而後進行下一趟比較

例:主串 S = 「abc520abc520abcd」, 子串 T = "abc520abcd" ,利用 KMP算法匹配過程

子串 next 數組

j 0 1 2 3 4 5 6 7 8 9
子串 a b c 5 2 0 a b c d
next[j] -1 0 0 0 0 0 0 1 2 3

能夠看到,在 指針 i = 9 且 j = 9 的時候,匹配失敗, 此時 next[9] = 3 ,因此子串指針回溯到 下標 j = 3 的位置也就是元素 5 的位置,進行第二輪比較,而後正好所有匹配成功

(三) 求next數組算法實現

void Stirng::getNext(const String &t, int *next) {
    int i = 0, j = -1;
    next[0] = -1;
    while (i < t.curLength - 1) {
        if ((j == -1) || t[i] == t[j]) {
            ++i, ++j;
            next[i] = j;
        }else{
            j = next[j];
        }
    }
}

KMP算法代碼實現

有了 next 數組的鋪墊,咱們就能夠來實現KMP算法了

匹配成功返回子串在主串中第一次出現的位置,失敗返回-1,子串爲空串返回0

int String::kmpFind(const String &t, int pos) {
    //不容許申請大小爲0的數組
    if (t,curLength == 0) return 0;
    //若是主串比子串小,匹配失敗
    if(t.curLength < t.curLength) return -1;
    //主串指針i,子串指針j
    int i = 0, j = 0;
    int *next = new int[t.curLrngth];
    getNext(t,next);
    while (i < curLength && j < t,curLength) {
        if (j == -1 || data[i] == t.data[j])    //狀況12
            i++, j++;
        else    //狀況3
            j = next[j];
    }
    delete []next;
    if (j > t.curLength)
        return (i - t.curLength)
    else
        return -1;
}

KMP模式匹配算法改進

有一種特殊狀況的出現,使得咱們不得不考慮KMP算法的改進

那就是子串中有多個連續重複的元素,例如主串 S=「aaabcde」 子串T=「aaaaax」 在主串指針不動,移動子串指針比較這些值,其實有不少無用功,由於子串中前5個元素都是相同的a,因此咱們能夠省略掉這些重複的步驟

void String::getNextVal(const String &t, int *nextVal) {
    int i = 0, j = -1;
    nextVal[0] = -1;
    while (i < t.curLength -1) {
        if ((k == -1) || (t[i] == t[j])) {
            ++i, ++j;
            if (t[i] != t[j])
                nextVal[i] = j;
            else
                nextVal[i] = nextVal[j];
        }
        else 
            j = nextVal[j];
    }
}

這種改進的核心就在於 增長了對子串中 t[i] 和 t[j] 是否相等的判斷,相等則直接將 nextVal[j] 的值賦給 nextVal[i]

總結

在BF算法中,當主串和子串不匹配的時候,主串和子串你的指針都須要回溯,因此致使了該算法時間複雜度比較高爲 O(nm) ,空間複雜度爲 O(1) 注:雖然其時間複雜度爲 O(nm) 可是在通常應用下執行,其執行時間近似 O(n+m) 因此仍被使用

KMP算法,利用子串的結構類似性,設計next數組,在此之上達到了主串不回溯的效果,大大減小了比較次數,可是相對應的卻犧牲了存儲空間,KMP算法 時間複雜度爲 O(n+m) 空間複雜度爲 O(n)

結尾:

若是文章中有什麼不足,或者錯誤的地方,歡迎你們留言分享想法,感謝朋友們的支持!

若是能幫到你的話,那就來關注我吧!若是您更喜歡微信文章的閱讀方式,能夠關注個人公衆號

在這裏的咱們素不相識,卻都在爲了本身的夢而努力 ❤

一個堅持推送原創開發技術文章的公衆號:理想二旬不止

相關文章
相關標籤/搜索