4種字符串匹配算法:BS朴素 Rabin-karp(上)


  字符串的匹配的算法一直都是比較基礎的算法,我們本科數據結構就學過了嚴蔚敏的KMP算法。KMP算法應該是最高效的一種算法,但是確實稍微有點難理解。所以打算,開這個博客,一步步的介紹4種匹配的算法。也是《算法導論》上提到的。我會把提到的四種算法全部用c/c++語言實現。提供參考學習。下圖的表格,介紹了各個算法的處理時間和匹配時間。希望我寫的比較清楚。如果不理解的,或者不對的,歡迎留言。

字符串匹配算法及其處理時間和匹配時間
算法 預處理時間 匹配時間
朴素算法 0 O((n-m+1)m)
Rabin-Karp ⊙(m) O((n-m+1)m)
有限自動機算法 O(m|∑|) ⊙(n)
KMP(Knuth-Morris-Pratt) ⊙(m) ⊙(n)

====BF算法(朴素的模式匹配)======================================================

  介紹,上面這四個算法之前,和所有的教材一下,先介紹一下BF算法吧(暴力算法)。

  它的思路很簡單:把每個字符串都拿來做對比。時間復雜度是O(m*n)。我們不妨先看看代碼:

 1 char* strStr(const char* str,const char* target)
 2 {
 3     if(!*target) return str;
 4     char *p1 = (char *)str;
 5     
 6      while(*p2)
 7     {
 8         char *p1begin = p1;
 9         char *p2 = (char *)target;
10         while(*p1 && *p2 && (*p1 == *p2))
11         {
12             p1++;
13             p2++;
14         }
15         if(!*p2) return p1begin;
16     }
17     return NULL;
18 }

                                                              圖解:

  上圖是我通過上面的代碼畫的一張偏於理解的圖:第10行~14行是重點代碼,為別進行++的操作,做對比,並且指針一直往后指。直到T串操作完成。最后判斷是否T串是否已經走到末尾,如果已經走到末尾,代表了S串中包含了T串的內容,則返回保存的指針。

  該算法的時間復雜度:兩個while循環,所以O(m*n)。

  BF算法是屬於朴素算法的,算法導論中對朴素算法的偽代碼是這樣的:

1 n=T.length
2 m=P.lenth
3 for s = 0 to n-m
4     if P[1...m] == T [s+1...s+m]
5     print "pattern occurs with shift"s

  什么 意思?

  其實他只要對比n-m+1次即可。我們以上圖的圖作為例子。n-m=12,for循環從0~12只需要對比13次,即(n-m+1),如果第十三次都不成功,說明S串中沒有T,直接返回NULL,他的時間復雜度是O(n-m+1),但是如果存在,並且在最后一位,那么他的時間復雜度就是O((n-m+1)*m),找到以后,還要進行m次對比,也就是兩個while循環。

  因此,我得出結論,BF算法應該是朴素算法里的一種。我們把這類的算法都成為朴素的模式匹配算法。

  當然,如果模式T,所有的字符都不同,則有沒有方法能夠將朴素算法降到O(n),答案是肯定有的。(另外說一句,他們的時間復雜度都很好計算,如果不會的話,就去看看簡單的參考書)。

 1 int strStr1(const char* str, const char* target)
 2 {
 3     for(i = 0,j = 0; i != n; i++)
 4     {
 5         if(str[i] == target[j]) 
 6         {
 7             j++;
 8         }
 9         else
10         {
11             j = 0;
12         }
13         if(j == m)
14             return true;
15     }
16 }
  這道題是算法導論第三版的32章32.1.2的題目,但是我覺得這題目要考慮一點,如果我要返回的是S串中在什么位置,也就是想要返回一個S串中的指針,上面的方法顯然是不行的。因為他只是返回:存不存在這個串。
  那如何改進呢?
char* strStr1(const char* str, const char* target)
{
    int i, j;
    int n = strlen(str);
    int m = strlen(target);
    char *p1 = (char*)str;
    for (i = 0, j = 0; i != n; i++)
    {
        if (str[i] == target[j])
        {
            j++;
        }
        else
        {
            j = 0;
        }
        if (j == m)
            return (p1+i-j+1);//返回該位置的地址
    }
}

Git:代碼下載:brute_force.c

====Rabin-Karp=================================================

 關於Rabin-Karp算法,會比較復雜。因為涉及到了一些數學上的知識,用到了一些進制的轉換。也扯到了模等。我覺得要理解他,如果全憑看算導上面的東西,肯定會把自己弄暈的。我們先來看一篇博文(點擊跳轉),之后,我們找道題目練練手(poj 1200)。答案在后面給出:

Rabin-Karp 字符串搜索算法 是一個相對快速的字符串搜索算法,它所需要的平均搜索時間是O(n).這個算法是建立在使用散列來比較字符串的基礎上的。

Rabin-Karp算法在字符串匹配中其實也不算是很常用,但它的實用性還是不錯的,除非你的運氣特別差,最壞情況下可能會需要O((n-m)*m)的運行時間(關於n,m的意義請看上篇)。平均情況下,還是比較好的。

朴素的字符串匹配算法為什么慢?因為它太健忘了,前一次匹配的信息其實可以有部分可以應用到后一次匹配中的,而朴素的字符串匹配算法只是簡單的把這個信息扔掉,從頭再來,因此,浪費了時間。好好的利用這些信息,自然可以提高運行速度。

這個算法不是那么容易說清楚,我舉一個例子說下(看算法導論看到的例子)。

我們用E來表示字母表的字母個數,這個例子字母表如下:{0,1,2,3,4,5,6,7,8,9},那么E就是10,如果采用小寫英文字母來做字母表,那么E就是26,類此。

由於完成兩個字符串的比較需要對其中包含的字符進行檢驗,所需的時間較長,而數值比較則一次就可以完成,那么我們首先把模式(匹配的字串)轉化成數值(轉化成數值的好處不僅僅在此)。在這個例子里我們可以把字符0~9映射到數字0~9。比如,”423″,我們可以轉化成3+E*(2+E*4)),這樣一個數值,如果這個值太大了,我們可以選一個較大的質數對其取模,模后的值作為串的值。

這邊處理好了,那么接下來轉換被匹配的字符串,取前m個字符,如上述操作對其取值,然后對該值進行比較即可。

若不匹配,則繼續向下尋找,這時候該如何做呢?比如模式是”423″,而父串是”324232″;第一步比較423跟324的值,不相等,下一步應該比較423跟242了,那么我們這步如何利用前一步的信息呢?首先我們把324前去300,然后在乘以E(這里是10),在加上2不就成了242了么?用個式子表示就是新的值a(i+1)=(E(a(i)-S[i])*h-S[S+M])) MOD p,p是我們選取的大質數,S[i]表示父串的第i個字符,而a(i)表示當前值,本例中就是324,h表示當前值最高位的權值,比如,324,則h=100,就是3這個位的權值,形式化的表示就是h=(E^m-1)MOD p。當然拉,由於采用了取模操作,當兩者相等時,未必是真正的相等,我們需要進行細致的檢查(進行一次朴素的字符串匹配操作)。若不相等,則直接可以排除掉。繼續下一步。

 答案:

#include <stdio.h>
#include <string.h>
char str[1000000];
bool hash[16000000] = {false};
int ansi[256] = {0};

int main(){
    int N, NC, ans = 0;    
    scanf("%d%d%s",&N, &NC, str);
    for(char *s = str; *s; ++s){    //*s 不是 s 
        ansi[*s] = 1;        //如果字母出現過,賦值為1
    }
    int cnt = 0;
    for(int i = 0; i < 256; ++i){
        if(ansi[i])
            ansi[i] = cnt++;    //從0開始編號
    }
    int len = strlen(str);
    for(int i = 0; i < len - N + 1; ++i){
        int key = 0;
        for(int j = 0; j < N; ++j){
            key = key * NC + ansi[str[i + j]];    //轉換成NC進制
            //printf("%d\n",ansi[str[i + j]]);
        }
        //printf("key=%d\n",key);
        if( !hash[key] ){
            ans++;
            hash[key] = true;
        }
    }
    printf("%d\n",ans);
    return 0;
}
答案 點擊打開

  如果你認認真真的做了,那肯定對該算法有了一些簡單的理解。然后我們再去分析《算導》上的偽代碼,以及一些知識點。我們先來看算導上的圖。

 

  31415的模是7,14152的模是8,67399的模也是7,但是,這兩個數並不是相同的數,因此,他是一個偽命中點。模的計算機步驟在圖C中已經寫的很清楚了,不必多費口舌。但是當我們遇到兩個相同的mod的時候,有必要去對比,判斷他是否是和子串相同的。如果相同,則命中,判斷他的串(值)是否相同。相同,則真命中,否則為偽命中。

  他預處理時間為o(m),原因是他要將各個數的轉化為模(下有偽代碼中可看出)需要有一段時間,轉化的時間是o(m)。之后,只要對比n-m+1次即可。如果兩個數相同,則進行m次匹配。因此他最壞的時間復雜度是o(m*(n-m+1))。

  我們來看看偽代碼: 

n = len(T);
m = len(p);
h = d^(m-1)mod q; //表示進位
p = 0;
t0 = 0;
for i   1 to m  //m次預處理時間
   p = (d*p+P[I]) mod q;
   t0 = (d*t0+T[I])mod q;
for i 0 to n-m   //從串S里面開始逐個搜索
     if p==ti
     else ts+1 = d(ts-T[S+1]h) + T[S+M+1]mod q //比較重要的一步。獲取下一個mod

===================

注:有限自動機算法、KMP請關注下。轉載請注明出處。


免責聲明!

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



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