1. 程式人生 > >常用算法3 - 字符串查找/模式匹配算法(BF & KMP算法)

常用算法3 - 字符串查找/模式匹配算法(BF & KMP算法)

urn 得出 code input 失敗 相等 復雜度 acc logs

相信我們都有在linux下查找文本內容的經歷,比如當我們使用vim查找文本文件中的某個字或者某段話時,Linux很快做出反應並給出相應結果,特別方便快捷!
那麽,我們有木有想過linux是如何在浩如煙海的文本中正確匹配到我們所需要的字符串呢?這就牽扯到了模式匹配算法!

1. 模式匹配

什麽是模式匹配呢?

  • 模式匹配,即子串P(模式串)在主串T(目標串)中的定位運算,也稱串匹配

假設我們有兩個字符串:T(Target, 目標串)和P(Pattern, 模式串);在目標串T中查找模式串T的定位過程,稱為模式匹配.

模式匹配有兩種結果:

  • 目標串中找到模式為T的子串,返回P在T中的起始位置下標值;
  • 未成功匹配,返回-1

通常模式匹配的算法有很多,比如BF、KMP、BM、RK、SUNDAY等等,它們各有千秋,我們此處重點講解BF和KMP算法(因為比較常用)

2. BF算法

BF,即Brute-Force算法,也稱為樸素匹配算法蠻力算法,效率較低!

1). 算法思想

基本思想:

    1. 將目標串T第一個字符與模式串P的第一個字符比較;
    1. 若相等,則比較T和P的第二個字符
    1. 若不等,則比較T的下一個字符與P的第一個字符
    1. 重復步驟以上步驟,直到匹配成功或者目標串T結束

流程圖如下:
技術分享圖片

例如:
T=‘ababcabcacbab‘, P=‘abcac‘, 匹配流程
技術分享圖片

  • Step 1:
    主串T與子串P做順序比較,當比較到位置2時,主串T[2]=‘a‘與子串P[2]=‘c‘不等(藍色陰影表示),記錄各自的結束位置,並進入Step 2
  • Step 2: 主串T後移一位,主串T與子串P再從頭開始比較,比較如Step 1
  • Step 3: 每次比較,子串都從0開始,主串的開始位置與上次的結束位置存在一定的關系;在某些時候需要“回溯”(上次比較結束的位置要向前移動);如Step 1的結束位置為2,Step 2的開始位置為1;Stp3的結束位置為6,Step 4的開始位置為3等;
  • Step 4: 主串T的索引值i 與 子串P的索引值j的關系為:i=i-j+1

2). 代碼實現

/*-----------------------------------------------------------------------------
 * Function: BF - Does the P can be match in T
 * Input:   Pattern string P, Target string T
 * Output:  If matched: the index of first matched character
 *          else: -1
-----------------------------------------------------------------------------*/
int BF(const string &T, const string &P)
{
    int j=0, i=0, ret=0;

    while((j < P.length()) && (i<T.length()))
    {
        if(P[j] == T[i])    //字符串相等則繼續
        {
            i++;
            j++;        //目標串和子串進行下一個字符的匹配
        }
        else
        {
            i = i - j + 1;
            j = 0;              //如果匹配不成功,則從目標字符串的下一個位置開始從新匹配
        }
    }

    if(i < T.length())      //若匹配成功,返回匹配的第一個字符的下標值
        ret = i - P.length() ;
    else
        ret = -1;

    return ret;
}

3). 效率分析

效率分析主要是分析時間復雜度和空間復雜度. 而本例的空間復雜度較低,暫時不做考慮,我們來看看時間復雜度。
分析時間復雜度通常是分析最壞情況,對於BF算法來說,最壞情況舉例如下:
T="ggggggggk", P="ggk"

技術分享圖片
由上圖可知,第i次匹配,前面第i-1次匹配,每次都需要比較m次(m為模式串P的長度),因此為(i-1)m次;第i次匹配成功也需要m次比較,因此總共需要比較mi次。

對於長度為n的主串T,i=n-m+1,每次匹配成功的概率為Pi,且概率相等;則在最壞情況下,匹配成功的概率Cmax可表示為:

技術分享圖片

一般情況下 n>>m,因此,BF的時間復雜度為 O(m*n)

3. KMP算法

BF算法每次都需要回溯,導致時間復雜度較大,那麽有沒有一種效率更高的模式匹配算法呢?
答案是肯定的,那就是KMP算法。

1). 名詞解釋

在進行算法講解之前,必須要明確以下幾個名詞,否則無法理解此算法

  • 目標串 T: 即大量的等待被匹配的字符串
  • 模式串 P:即我們需要查找的字符串
  • 字符串前綴:字符串的任意首部(不包括最後一個字符);如"abcd"的前綴為"a","ab","abc",但不包括"abcd"
  • 字符串後綴:字符串的任意尾部(不包括第一個字符);如"abcd"的後綴為"d","cd","bcd",但不包括"abcd"
  • 字符串前後綴相等位數k:即前綴與後綴的最長匹配位數,技術分享圖片

2). 算法思想

KMP算法的核心思想是:部分匹配,即不再把主串的位置移動到已經比較過的位置(不再回溯),而是根據上一次比較結果繼續後移。
概念相當抽象,那麽我們以例子來解釋:
技術分享圖片

  • Step 1: 匹配到索引值index=2時,匹配失敗
  • Step 2: 匹配的開始位置為index=2(沒有回溯到1), 原因如下:

    Step 1 比較後,已知T[1]=‘b‘, S[0]=‘a‘,理論上已經比較過了,所以無需回溯再次比較

Step 2 一直進行匹配,直到T[6]時刻失配.

  • Step 3: T的位置不進行回溯,還是保持在T[6]開始(KMP算法規定:目標串T不回溯,上一次的結束位置即為下一次的開始位置);
    P的索引值從1開始而非0,原因如下:

    在Step 2 中,T[5]=‘a‘已經比較過,我們已知,且與P[3]相等;因為P[0]==P[3],所以無需比較P[0]與T[5],因為Step 2 理論上已經進行了比較(其實就是看子串P Step2結束位置P[4]之前的P[0-3]的字符串前後綴相等位數k,使得P[k]與上次主串的結束位置T[6]對齊)

由以上分析可知,KMP算法過程中關鍵點就是求: 子串P結束位置前的前後綴相等位數k
下圖是模式串P="abcabca"的前後綴關系分析(包括前後綴字符串相等位數k)
技術分享圖片

由上圖我們可以給出,T串每一個字符做結束位置時,下一次的開始位置的值;

  • j 為T的本次匹配結束位置(失配位置);
  • next[j] 為下次匹配模式串P的開始位置

技術分享圖片
PS: next[j]就是前後綴字符串相等位數k

根據上面的討論,我們可以得出next[j]的運算公式:
技術分享圖片
其中,-1 是一個標記,標識下一次的開始位置目標串為技術分享圖片,模式串P為技術分享圖片

如果以上你沒有明白,不要緊的,只需要記住next[j]的函數就可以,其它一切都是根據它來的!

3). 代碼實現

/*-----------------------------------------------------------------------------
 * Function: KMP- Does the P can be match in T
 * Input:   Pattern string P, array next
 * Output:  If matched: the index of first matched character
 *          else: -1
-----------------------------------------------------------------------------*/
void getNext(const string &P, int next[])
{
    int j=0;    //模式串P的下標值/索引值
    int k=-1;   //模式串P的前綴和後綴串相等的位數
    next[0]=-1; //置初值

    while(j < P.length())
    {
        if((k == -1) || (P[j] == P[k])) //從模式串P的開始位置處理 或 順序比較主串和子串
        {
            j++;
            k++;
            next[j] = k;
        }

        else            //設置重新比較位置:j串不變,k串從next[k]位置開始
            k = next[k];
    }
}

/*-----------------------------------------------------------------------------
 * Function: KMP- Does the P can be match in T
 * Input:   Pattern string P, Target string T
 * Output:  If matched: the index of first matched character
 *          else: -1
-----------------------------------------------------------------------------*/
int KMP(const string &T, const string &P)
{
    int next[MaxSize]={0};
    int i=0;    //目標串T的下標值/索引值
    int j=0;    //模式串P的下標值/索引值
    int ret=0;

    getNext(P, next);   //獲取模式串P的next數組

    int PLen = P.length();
    int TLen = T.length();

    while((i < T.length()) && (j < PLen))   //奇怪,此處我用 j<P.length()就不行,待解決
    {
        if((j==-1) || (P[j] == T[i]))   //j=-1表示首次比較
        {
            i++;
            j++;
        }

        else
        {
            j = next[j];
        }
    }

    if(j >= P.length())
        ret = i-P.length();
    else
        ret = -1;

    return ret;
}

4). 效率分析

由於KMP算法不回溯,比較是順序進行的,因此最壞情況下的KMP時間復雜度為 O(m+n).
其中,m為模式串P的字符串長度,n為目標串T的字符串長度.

常用算法3 - 字符串查找/模式匹配算法(BF & KMP算法)