1. 程式人生 > 實用技巧 >字串匹配演算法綜述

字串匹配演算法綜述

寫的好棒!!!%%%粘來咯...

字串匹配演算法,是在實際工程中經常遇到的問題,也是各大公司筆試面試的常考題目。此演算法通常輸入為原字串(string)和子串(pattern),要求返回子串在原字串中首次出現的位置。比如原字串為“ABCDEFG”,子串為“DEF”,則演算法返回3。常見的演算法包括:BF(Brute Force,暴力檢索)、RK(Robin-Karp,雜湊檢索)、KMP(教科書上最常見演算法)、BM(Boyer Moore)、Sunday等,下面詳細介紹。

1 BF演算法:
暴力檢索法是最好想到的演算法,也最好實現,在情況簡單的情況下可以直接使用:


首先將原字串和子串左端對齊,逐一比較;如果第一個字元不能匹配,則子串向後移動一位繼續比較;如果第一個字元匹配,則繼續比較後續字元,直至全部匹配。
時間複雜度:O(MN)

2 RK演算法:
RK演算法是對BF演算法的一個改進:在BF演算法中,每一個字元都需要進行比較,並且當我們發現首字元匹配時仍然需要比較剩餘的所有字元。而在RK演算法中,就嘗試只進行一次比較來判定兩者是否相等。
RK演算法也可以進行多模式匹配,在論文查重等實際應用中一般都是使用此演算法。

首先計運算元串的HASH值,之後分別取原字串中子串長度的字串計算HASH值,比較兩者是否相等:如果HASH值不同,則兩者必定不匹配,如果相同,由於雜湊衝突存在,也需要按照BF演算法再次判定。

按照此例子,首先計運算元串“DEF”HASH值為Hd,之後從原字串中依次取長度為3的字串“ABC”、“BCD”、“CDE”、“DEF”計算HASH值,分別為Ha、Hb、Hc、Hd,當Hd相等時,仍然要比較一次子串“DEF”和原字串“DEF”是否一致。
時間複雜度:O(MN)(實際應用中往往較快,期望時間為O(M+N))

3 KMP演算法:
字串匹配最經典演算法之一,各大教科書上的看家絕學,曾被投票選為當今世界最偉大的十大演算法之一;但是晦澀難懂,並且十分難以實現,希望我下面的講解能讓你理解這個演算法。

KMP演算法在開始的時候,也是將原字串和子串左端對齊,逐一比較,但是當出現不匹配的字元時,KMP演算法不是向BF演算法那樣向後移動一位,而是按照事先計算好的“部分匹配表”中記載的位數來移動,節省了大量時間。這裡我借用一下阮一峰大神的例子來講解:

首先,原字串和子串左端對齊,比較第一個字元,發現不相等,子串向後移動,直到子串的第一個字元能和原字串匹配。

當A匹配上之後,接著匹配後續的字元,直至原字串和子串出現不相等的字元為止。

此時如果按照BF演算法計算,是將子串整體向後移動一位接著從頭比較;按照KMP演算法的思想,既然已經比較過了“ABCDAB”,就要利用這個資訊;所以針對子串,計算出了“部分匹配表”如下(具體如何計算後面會說,這個先介紹整個流程):

剛才已經匹配的位數為6,最後一個匹配的字元為“B”,查表得知“B”對應的部分匹配值為2,那麼移動的位數按照如下公式計算:
移動位數 = 已匹配的位數 - 最後一個匹配字元的部分匹配值
那麼6 - 2 = 4,子串向後移動4位,到下面這張圖:

因為空格和“C”不匹配,已匹配位數為2,“B”對應部分匹配值為0,所以子串向後移動2-0=2位。

空格和“A”不匹配,已匹配位數為0,子串向後移動一位。

逐個比較,直到發現“C”與“D”不匹配,已匹配位數為6,“B”對應部分匹配值為2,6-2=4,子串向後移動4位。

逐個比較,直到全部匹配,返回結果。
下面說明一下“部分匹配表”如何計算,“部分匹配值”是指字串字首和字尾所共有元素的長度。字首是指除最後一個字元外,一個字串全部頭部組合;字尾是指除第一個字元外,一個字串全部尾部組合。以”ABCDABD”為例:
“AB”的字首為[A],字尾為[B],共有元素的長度為0;
“ABC”的字首為[A, AB],字尾為[BC, C],共有元素的長度0;
“ABCD”的字首為[A, AB, ABC],字尾為[BCD, CD, D],共有元素的長度為0;
“ABCDA”的字首為[A, AB, ABC, ABCD],字尾為[BCDA, CDA, DA, A],共有元素為”A”,長度為1;
“ABCDAB”的字首為[A, AB, ABC, ABCD, ABCDA],字尾為[BCDAB, CDAB, DAB, AB, B],共有元素為”AB”,長度為2;
“ABCDABD”的字首為[A, AB, ABC, ABCD, ABCDA, ABCDAB],字尾為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。
在計算“部分匹配表”時,一般使用DP(動態規劃)演算法來計算(表示為next陣列)://這裡我沒看懂,理論上不用DP直接搜也行啊

        int* next = new int[needle.length()];
        next[0] = 0;
        int k = 0;
        for (int i = 1; i < needle.length(); i++)
        {
            while (k > 0 && needle[i] != needle[k])
            {
                k = next[k - 1];
            }
            if (needle[i] == needle[k])
            {
                k++;
            }
            next[i] = k;
        }

時間複雜度:O(N)

4 BM演算法:
在本科的時候,我一直認為KMP演算法是最好的字串匹配演算法,直到後來我遇到了BM演算法。BM演算法的執行效率要比KMP演算法快3-5倍左右,並且十分容易理解。各種記事本的“查詢”功能(CTRL + F)一般都是採用的此演算法。
網上所有講述這個演算法的帖子都是以傳統的“好字元規則”和“壞字元規則”來講述的,但是個人感覺其實這樣不容易理解,我總結了另外一套簡單的演算法規則:
我們拿這個演算法的發明人Moore教授的例子來講解:

首先,原字串和子串左端對齊,但是從尾部開始比較,就是首先比較“S”和“E”,這是一個十分巧妙的做法,如果字串不匹配的話,只需要這一次比較就可以確定。
在BM演算法中,當每次發現當前字元不匹配的時候,我們就需要尋找一下子串中是否有這個字元;比如當前“S”和“E”不匹配,那我們需要尋找一下子串當中是否存在“S”。發現子串當中並不存在,那我們將子串整體向後移動到原字串中“S”的下一個位置(但是如果子串中存在原字串當前字元腫麼辦呢,我們後面再說):

我們接著從尾部開始比較,發現“P”和“E”不匹配,那我們查詢一下子串當中是否存在“P”,發現存在,那我們就把子串移動到兩個“P”對齊的位置:

已然從尾部開始比較,“E”匹配,“L”匹配,“P”匹配,“M”匹配,“I”和“A”不匹配!那我們就接著尋找一下子串當前是否出現了原字串中的字元,我們發現子串中第一個“E”和原字串中的字元可以對應,那直接將子串移動到兩個“E”對應的位置:

接著從尾部比較,發現“P”和“E”不匹配,那麼檢查一下子串當中是否出現了“P”,發現存在,那麼移動子串到兩個“P”對應:

從尾部開始,逐個匹配,發現全部能匹配上,匹配成功~
時間複雜度:最差情況O(MN),最好情況O(N)

5 Sunday演算法:
後來,我又發現了一種比BM演算法還要快,而且更容易理解的演算法,就是這個Sunday演算法:

首先原字串和子串左端對齊,發現“T”與“E”不匹配之後,檢測原字串中下一個字元(在這個例子中是“IS”後面的那個空格)是否在子串中出現,如果出現移動子串將兩者對齊,如果沒有出現則直接將子串移動到下一個位置。這裡空格沒有在子串中出現,移動子串到空格的下一個位置“A”:

發現“A”與“E”不匹配,但是原字串中下一個字元“E”在子串中出現了,第一個字元和最後一個字元都有出現,那麼首先移動子串靠後的字元與原字串對齊:

發現空格和“E”不匹配,原字串中下一個字元“空格”也沒有在子串中出現,所以直接移動子串到空格的下一個字元“E”:

這樣從頭開始逐個匹配,匹配成功!
時間複雜度:最差情況O(MN),最好情況O(N)

//實際我寫好像可以是o(M+N)啊。。

程式碼粘一下:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
char a[10005],b[10005];//long a>long b
int c[30];//表示b串中存在的字母;不存在則為1,存在為最靠後的此字元距離尾部加一(要跳的地方) 
int la,lb;//字串a,b的長度 
int head;//當前搜尋到的頭字元 
int main()
{
    scanf("%s",a);
    scanf("%s",b);//read in
    la=strlen(a);
    lb=strlen(b); 
    for(int i=0;i<=lb-1;i++)
        c[b[i]-'a'+1]=lb-i;//初始化c陣列 
    for(int i=0;head<=la-1;)//i表示當前匹配長度 ,head指標跳到a尾時結束 
    {
        if(a[head+i]==b[i])
        {
            i++;//匹配則更新i值
            if(i==lb) //匹配到的長度等於b串長度 則成功 
            {
                printf("Yes");return 0;
            }
        }        
        else
        {
            if(c[a[head+lb]-'a'+1]!=0) head=head+c[a[head+lb]-'a'+1];//判斷是否出現
            else head=head+lb+2; //未出現,跳到下一個長度 
            i=0;//匹配值更新為0
        }         
    }
    printf("No");
    return 0;
}

轉載於:https://www.cnblogs.com/Franky-ln/p/5890201.html