本文內容框架:
§1 Boyer-Moore算法
§2 Horspool算法
§3 Sunday算法
§4 KMP算算法
§5 KR算法
§6 AC自動機
§7 小結
§1 Boyer-Moore(BM)算法
Boyer-Moore算法原理
Boyer-Moore算法是一種基於後綴匹配的模式串匹配算法,後綴匹配就是模式串從右到左開始比較,但模式串的移動還是從左到右的。字符串匹配的關鍵就是模式串的如何移動纔是最高效的,Boyer-Moore爲了做到這點定義了兩個規則:壞字符規則和好後綴規則,下面圖解給出定義:
下面分別針對利用壞字符規則和好後綴規則移動模式串進行介紹:
壞字符規則
1.如果壞字符沒有出現在模式字符中,則直接將模式串移動到壞字符的下一個字符:
(壞字符c,沒有出現模式串P中,直接將P移動c的下一個位置)
2.如果壞字符出現在模式串中,則將模式串最靠近好後綴的壞字符(當然這個實現就有點繁瑣)與母串的壞字符對齊:
(注:如果模式串P是babababab,則是將第二個b與母串的b對齊)
好後綴規則
好後綴規則分三種情況
1.模式串中有子串匹配上好後綴,此時移動模式串,讓該子串和好後綴對齊即可,如果超過一個子串匹配上好後綴,則選擇最靠靠近好後綴的子串對齊。
2.模式串中沒有子串匹配上後後綴,此時需要尋找模式串的一個最長前綴,並讓該前綴等於好後綴的後綴,尋找到該前綴後,讓該前綴和好後綴對齊即可。
其實,1和2都可以看成模式串還含有好後綴串(好後綴子串也是好後綴)。
3.模式串中沒有子串匹配上後後綴,並且在模式串中找不到最長前綴,讓該前綴等於好後綴的後綴。此時,直接移動模式到好後綴的下一個字符。
Boyer-Moore算法步驟
1.對模式子串進行預處理
Boyer-Moore算法實現必須對模式串進行預處理,得到壞字符規則和好後綴規則移動的映射表,下面代碼中MakeSkip是建立壞字符規則移動的映射表,MakeShift是建立好後綴規則的移動映射表。
MakeSkip是構造數組skip[],skip[k]表示字符k距離模式串末尾的距離。
MakeShfit是構造數組shfit[],shfit[k]表示模式串的以k爲邊界的後綴子串的最靠近的模式子串(或最前綴子串)到模式子串末尾的距離,例如:abcab,shfit[3]=3和shfit[2]=3(即都是第一個b到末尾的距離),k=2時,後綴子串爲cab,這時只有最長前綴ab,shfit[2]=3。
2.從b_idx開始查找,得到壞字符和好後綴,得到最大移動距離,移動b_idx,直至b_idx到達母串的末尾。
Boyer-Moore算法實現
╔
- /*
- 函數:int* MakeSkip(char *, int)
- 目的:根據壞字符規則做預處理,建立一張壞字符表
- 參數:
- ptrn => 模式串P
- PLen => 模式串P長度
- 返回:
- int* - 壞字符表
- */
- int* MakeSkip(char *ptrn, int pLen)
- {
- int i;
- //爲建立壞字符表,申請256個int的空間
- /*PS:之所以要申請256個,是因爲一個字符是8位,
- 所以字符可能有2的8次方即256種不同情況*/
- int *skip = (int*)malloc(256*sizeof(int));
- if(skip == NULL)
- {
- fprintf(stderr, "malloc failed!");
- return 0;
- }
- //初始化壞字符表,256個單元全部初始化爲pLen,沒有在模式串出現的字符距離爲pLen。
- for(i = 0; i < 256; i++)
- {
- *(skip+i) = pLen;
- }
- //給表中需要賦值的單元賦值,不在模式串中出現的字符就不用再賦值了
- while(pLen != 0)
- {
- *(skip+(unsigned char)*ptrn++) = pLen--;
- }
- return skip;
- }
- /*
- 函數:int* MakeShift(char *, int)
- 目的:根據好後綴規則做預處理,建立一張好後綴表
- 參數:
- ptrn => 模式串P
- PLen => 模式串P長度
- 返回:
- int* - 好後綴表
- */
- int* MakeShift(char* ptrn,int pLen)
- {
- //爲好後綴表申請pLen個int的空間
- int *shift = (int*)malloc(pLen*sizeof(int));
- int *sptr = shift + pLen - 1;//方便給好後綴表進行賦值的指標
- char *pptr = ptrn + pLen - 1;//記錄好後綴表邊界位置的指標
- char c;
- if(shift == NULL)
- {
- fprintf(stderr,"malloc failed!");
- return 0;
- }
- c = *(ptrn + pLen - 1);//保存模式串中最後一個字符,因爲要反覆用到它
- *sptr = 1;//以最後一個字符爲邊界時,確定移動1的距離
- pptr--;//邊界移動到倒數第二個字符(這句是我自己加上去的,因爲我總覺得不加上去會有BUG,大家試試“abcdd”的情況,即末尾兩位重複的情況)
- while(sptr-- != shift)//該最外層循環完成給好後綴表中每一個單元進行賦值的工作
- {
- char *p1 = ptrn + pLen - 2, *p2,*p3;
- //該do...while循環完成以當前pptr所指的字符爲邊界時,要移動的距離
- do{
- while(p1 >= ptrn && *p1-- != c);//該空循環,尋找與最後一個字符c匹配的字符所指向的位置
- p2 = ptrn + pLen - 2;
- p3 = p1;
- while(p3 >= ptrn && *p3-- == *p2-- && p2 >= pptr);//該空循環,判斷在邊界內字符匹配到了什麼位置
- }while(p3 >= ptrn && p2 >= pptr);
- *sptr = shift + pLen - sptr + p2 - p3;//保存好後綴表中,以pptr所在字符爲邊界時,要移動的位置
- /*
- PS:在這裏我要聲明一句,*sptr = (shift + pLen - sptr) + p2 - p3;
- 大家看被我用括號括起來的部分,如果只需要計算字符串移動的距離,那麼括號中的那部分是不需要的。
- 因爲在字符串自左向右做匹配的時候,指標是一直向左移的,這裏*sptr保存的內容,實際是指標要移動
- 距離,而不是字符串移動的距離。我想SNORT是出於性能上的考慮,才這麼做的。
- */
- pptr--;//邊界繼續向前移動
- }
- return shift;
- }
- /*
- 函數:int* BMSearch(char *, int , char *, int, int *, int *)
- 目的:判斷文本串T中是否包含模式串P
- 參數:
- buf => 文本串T
- blen => 文本串T長度
- ptrn => 模式串P
- PLen => 模式串P長度
- skip => 壞字符表
- shift => 好後綴表
- 返回:
- int - 1表示成功(文本串包含模式串),0表示失敗(文本串不包含模式串)。
- */
- int BMSearch(char *buf, int blen, char *ptrn, int plen, int *skip, int *shift)
- {
- int b_idx = plen;
- if (plen == 0)
- return 1;
- while (b_idx <= blen)//計算字符串是否匹配到了盡頭
- {
- int p_idx = plen, skip_stride, shift_stride;
- while (buf[--b_idx] == ptrn[--p_idx])//開始匹配
- {
- if (b_idx < 0)
- return 0;
- if (p_idx == 0)
- {
- return 1;
- }
- }
- skip_stride = skip[(unsigned char)buf[b_idx]];//根據壞字符規則計算跳躍的距離
- shift_stride = shift[p_idx];//根據好後綴規則計算跳躍的距離
- b_idx += (skip_stride > shift_stride) ? skip_stride : shift_stride;//取大者
- }
- return 0;
- }
╝②
算法的時間複雜度最差(匹配不上)是O(n×m),最好是O(n),其中n爲母串的長度,m爲模式串的長度。BM算法時間複雜度最好是O(n/(m+1))
§2 Horspool算法
horspool算法將主串中匹配窗口的最後一個字符跟模式串中的最後一個字符比較。如果相等,繼續從後向前對主串和模式串進行比較,直到完全相等或者在某個字符處不匹配爲止(如下圖中的α與σ失配) 。如果不匹配,則根據主串匹配窗口中的最後一個字符β在模式串中的下一個出現位置將窗口向右移動。
Horspool算法相對於Boyer-Moore算法改進了壞字符規則,Boyer-Moore算法只是將模式串P中從當前未匹配位置向右第一個壞字符與母串的壞字符(未匹配的字符)對齊進行再次匹配,Horspool算法是以當前匹配窗口中母串的最末尾的一個字符和模式串最靠近它的字符對齊,下圖中β是當前匹配窗口的母串最後一個字符,將其與模式串左邊最靠近的β對齊移動。
Horspool算法預處理
爲了實現模式串的移動,必須先記錄每一個字符串在模式串中距離最右邊的距離:
Horspool算法實現
╔
- /*
- * implementation of Horspool
- * Author:Horspool
- * Coder: Cobbliu
- */
- #define WORD 26
- int horspool(char *T, int lenT, char *P, int lenP)
- {
- int d[WORD];
- int i, pos, j;
- for(i = 0; i != WORD; i++)
- d[i] = lenP;
- for(i = 0; i != (lenP-1); i++)
- d[P[i]-'A'] = lenP-i-1;
- pos = 0;
- while(pos < (lenT-lenP)){
- j = lenP-1;
- while(j >= 0 && T[pos+j]==P[j]) //matching
- j--;
- if(j == -1)
- return pos;
- else //not matched
- pos += d[T[pos+lenP-1]-'A'];
- }
- return -1;
- }
Horspool算法時間複雜度
假設主串的長度爲n,模式串的長度爲m,那麼Horspool算法最壞情況下的時間複雜度是O(mn),但平均情況下它的時間複雜度是O(n)。
╝④
§3 Sunday算法
Sunday算法思想跟BM算法很相似,在匹配失敗時關注的是文本串中參加匹配的最末位字符的下一位字符。如果該字符沒有在匹配串中出現則直接跳過,即移動步長= 匹配串長度+1;否則,同BM算法一樣其移動步長=匹配串中最右端的該字符到末尾的距離+1。
Sunday算法實現(不廢話直接上代碼)
╔
- #include <iostream>
- #include <cstring>
- using namespace std;
- int sunday(const char* src, const char* des)
- {
- int len_s = strlen(src);
- int len_d = strlen(des);
- int next[26] = {0};
- for (int j = 0; j < 26; ++j)
- next[j] = len_d + 1;
- for (int j = 0; j < len_d; ++j)
- next[des[j] - 'a'] = len_d - j; //記錄字符到最右段的最短距離+1
- //例如:des = "abcedfb"
- //next = {7 1 5 4 3 2 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8}
- int pos = 0;
- while (pos < (len_s - len_d + 1)) //末端對齊
- {
- int i = pos;
- int j;
- for (j = 0; j < len_d; ++j, ++i)
- {
- if (src[i] != des[j])
- {
- pos += next[src[pos + len_d] - 'a'];
- //不等於就跳躍,跳躍是核心
- break;
- }
- }
- if ( j == len_d )
- return pos;
- }
- return -1;
- }
- int main()
- {
- char src[]="abcdacdaahfacabcdabcdeaa";
- char des[]="abcde";
- cout<<sunday(src,des)<<endl;
- return 0;
- }
╝⑤
Boyer-Moore、Horspool、Sunday算法小結
Boyer-Moore、Horspool、Sunday算法都是基於後綴數組的匹配算法,區別在於移動的方式不一樣(好像網上有些都沒有說的Boyer-Moore算法的好後綴規則,有可能是優化方法吧,沒有去深究,抱歉)。下面給出三種方法的對比:
╔
0 1 2 3 4 5 6 7 8 9 ...
|
0 1 2 3 4 5 6 7 8 9 ...
|
0 1 2 3 4 5 6 7 8 9 ...
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(a) Boyer-Moore | (b) Horspool | (c) Sunday | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
In this example, t0, ..., t4 = a b c a b is the current text window that is compared with the pattern. Its suffix a b has matched, but the comparison c-a causes a mismatch. The bad-character heuristics of the Boyer-Moore algorithm (a) uses the "bad" text character c to determine the shift distance. The Horspool algorithm (b) uses the rightmost character b of the current text window. The Sunday algorithm (c) uses the character directly right of the text window, namely d in this example. Since d does not occur in the pattern at all, the pattern can be shifted past this position.
╝⑥
§4 Knuth-Morris-Pratt(KMP)算法
KMP算法是一種高效的前綴匹配算法,在傳統蠻力(BF)匹配算法的基礎上改進的地方在於每次移動的距離不是1可以是更大,沒有進行回溯,BF算法的時間複雜度是O(m*n),而KMP算法的時間複雜度是O(m+n)。
假設執行第i+1趟匹配時,如果比較模式串P中的第j個字符時不匹配,也就是有
T[i,i+1,...,i+j-1]=P[0,1,...,j-1],T[i+j]≠P[j] (打不了下標,就有數組的形式給出字符串) (1)
BF算法下一趟是從目標的第i+1位置開始與模式串比較。如果匹配成功則有
T[i+1,i+2,...,i+m]=P[0,1,...m-1] (2)
如果模式串P有如下特徵
P[0,1,...j-2]=P[1,2,...j-1] (3)
由(1)可知
T[i+1,i+2,...,i+j+1]=P[1,2,...j-1] (4)
由(3)(4)可知
T[i+1,i+2,...,i+j+1]≠P[0,1,...j-2] (5)
故由
T[i+1,i+2,....,i+m]≠P[0,1,...m-1]
所以第i+2趟是匹配可以不需要進行,因爲一定不能匹配。
類似可以推得
P[0,1,...k-1]=P[j-k-1,j-k,...j-1]
這時纔有
P[0,1,...k-1]=P[j-k-1,j-k,...j-1]=T[i+j-k,i+j-k+1,i+j-1]
模式串P從當前位置直接向右移動 j-k 位置,使模式串P的第 k 個字符P[k]與目標串T中的第i+j個字符對齊開始比較(前面 k 個已經匹配)。
造成BF算法效率低的主要原因是在算法執行過程中有回溯,而這些回溯是可以避免的。KMP算法的關鍵是在匹配失敗時,確定下一次匹配的位置,設next[j]=k,表示當模式串P中第j個字符與母串T相應字符不匹配時,模式串P中應當由第K個字符與目標串中剛不匹配的字符對齊繼續進行比較。
例如,模式串P="abaabcac",其對應的next[j]如下:
i |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
t[i] |
a |
b |
d |
a |
b |
c |
d |
e |
next[i] |
-1 |
0 |
0 |
0 |
1 |
2 |
0 |
0 |
next數組構造
╔ -1, j=0;
next[j]= ║max{k| 0<k<j 且 P[0,1,...,k-1]=P[j-k,j-k+1,..j-1}
╚ 0, 其他情況
next數組求解是一個遞推過程,
設next[j]=k,則有
P[0,1,...k-1]=P[j-k,j-k+1,...,j-1]
next[j]= ╔ max{k| 0<k<j 且 P[0,1,...,k]=P[j-k,j-k+1,..j-1}
╚ 0, 其他情況
如果P[k]=P[j],有 next[j+1]=next[j]+1=k+1。
如果P[k]≠P[j],有 P[0,1,...,k]≠P[j-k,j-k+1,...j],
假設next[j+1]=h+1,則有下式成立
P[0,1,...h]=P[j-h+1,j-k+1,...j] P[h]=P[j]
又因爲
P[0,1,...h-1]=P[j-h,j-k+1,...j-1]=P[k-h,k-h+1,k-1] (next[k]=h的情況)
即此時實際只需要滿足 next[k]=h(前面已經求解過)時,P[h]=P[j] 就有next[j+1]=h+1,否則(不存在這樣的h)next[j+1]等於0。
由此可以得到計算next的遞推公式
KMP算法實現
╔
- /* *******************************************************************
- created: 2006/07/02
- filename: KMP.cpp
- author: 李創
- http://www.cppblog.com/converse/
- 參考資料: 嚴蔚敏<<數據結構>>
- purpose: KMP字符串匹配算法的演示
- ******************************************************************** */
- #include < stdio.h >
- #include < stdlib.h >
- #include < assert.h >
- #include < string .h >
- #define MAX_LEN_OF_STR 30 // 字符串的最大長度
- typedef struct String // 這裏需要的字符串數組,存放字符串及其長度
- {
- char str[MAX_LEN_OF_STR]; // 字符數組
- int length; // 字符串的實際長度
- } String, * PString;
- // 得到字符串的next數組
- void GetNextArray(PString pstr, int next[])
- {
- assert(NULL != pstr);
- assert(NULL != next);
- assert(pstr -> length > 0 );
- // 第一個字符的next值是-1,因爲C中的數組是從0開始的
- next[ 0 ] = - 1 ;
- for ( int i = 0 , j = - 1 ; i < pstr -> length - 1 ; )
- {
- // i是主串的遊標,j是模式串的遊標
- // 這裏的主串和模式串都是同一個字符串
- if ( - 1 == j || // 如果模式串游標已經回退到第一個字符
- pstr -> str[i] == pstr -> str[j]) // 如果匹配成功
- {
- // 兩個遊標都向前走一步
- ++ i;
- ++ j;
- // 存放當前的next值爲此時模式串的遊標值
- next[i] = j;
- }
- else // 匹配不成功j就回退到上一個next值
- {
- j = next[j];
- }
- }
- }
- // KMP字符串模式匹配算法
- // 輸入: S是主串,T是模式串,pos是S中的起始位置
- // 輸出: 如果匹配成功返回起始位置,否則返回-1
- int KMP(PString S, PString T, int pos)
- {
- assert(NULL != S);
- assert(NULL != T);
- assert(pos >= 0 );
- assert(pos < S -> length);
- if (S -> length < T -> length)
- return - 1 ;
- printf( " 主串\t = %s\n " , S -> str);
- printf( " 模式串\t = %s\n " , T -> str);
- int * next = ( int * )malloc(T -> length * sizeof ( int ));
- // 得到模式串的next數組
- GetNextArray(T, next);
- int i, j;
- for (i = pos, j = 0 ; i < S -> length && j < T -> length; )
- {
- // i是主串游標,j是模式串游標
- if ( - 1 == j || // 模式串游標已經回退到第一個位置
- S -> str[i] == T -> str[j]) // 當前字符匹配成功
- {
- // 滿足以上兩種情況時兩個遊標都要向前進一步
- ++ i;
- ++ j;
- }
- else // 匹配不成功,模式串游標回退到當前字符的next值
- {
- j = next[j];
- }
- }
- free(next);
- if (j >= T -> length)
- {
- // 匹配成功
- return i - T -> length;
- }
- else
- {
- // 匹配不成功
- return - 1 ;
- }
- }
╝③
§5 Karp-Rabin(KR)算法
Karp-Rabin算法是利用hash函數的特性進行字符串匹配的。 KR算法對模式串和循環中每一次要匹配的子串按一定的hash函數求值,如果hash值相同,才進一步比較這兩個串是否真正相等。
Karp-Rabin算法適用於多個字符串匹配較好。
§6 Aho-Corasick算法
Aho-Corasick算法又叫AC自動機算法,是一種多模式匹配算法。Aho-Corasick算法可以在目標串查找多個模式串,出現次數以及出現的位置。
Aho-Corasick算法原理
Aho-Corasick算法主要是應用有限自動機的狀態轉移來模擬字符的比較,下面對有限狀態機做幾點說明:
上圖是由多模式串{he,she,his,hers}構成的一個有限狀態機:
1.該狀態當字符匹配是按實線標註的狀態進行轉換,當所有實線路徑都不滿足(即下一個字符都不匹配時)按虛線狀態進行轉換。
2.對ushers匹配過程如下圖所示:
當轉移到紅色結點時表示已經匹配並且獲得模式串
Aho-Corasick算法步驟
Aho-Corasick算法和前面的算法一樣都要對模式串進行預處理,預處理主要包括字典樹Tire的構造,構建狀態轉移表(goto),失效函數(failure function),輸出表(Output)。
Aho-Corasick算法包括以下3個步驟
1.構建字典樹Tire
2.構建狀態轉移表,失效函數(failure function),輸出表(Output)
3.搜索路徑(進行匹配)
下面3個步驟分別進行介紹
構建字典樹Tire
Tire是哈希樹的變種,Tire樹的邊是模式串的字符,結點就是Tire的狀態表,下圖是多模式串{he,she,his,hers}的Tire樹結構:
構建goto函數、failure function和Output函數
goto函數(狀態轉移函數):goto(pre,v)=next,完成這樣的任務:在當前狀態pre,輸入字符v,得到下一個狀態next,如果沒有下個狀態則next=failure。
failure function:失效函數是處理當前狀態是failure時的處理。
output函數:當完成匹配是根據狀態輸出匹配的模式串。
下面是多模式串{he,she,his,hers}的goto函數,failure函數,output函數
goto函數:
failure函數
output函數
多模式串{he,she,his,hers}最終的有限狀態機圖
Aho-Corasick算法實現
- ////////////////////////////////////////////////////
- /*
- 程序說明:多模式串匹配的AC自動機算法
- 自動機算法可以參考《柔性字符串匹配》裏的相應章節,講的很清楚
- */
- #include <stdio.h>
- #include <string.h>
- const int MAXQ = 500000+10;
- const int MAXN = 1000000+10;
- const int MAXK = 26; //自動機裏字符集的大小
- struct TrieNode
- {
- TrieNode* fail;
- TrieNode* next[MAXK];
- bool danger; //該節點是否爲某模式串的終結點
- int cnt; //以該節點爲終結點的模式串個數
- TrieNode()
- {
- fail = NULL;
- memset(next, NULL, sizeof(next));
- danger = false;
- cnt = 0;
- }
- }*que[MAXQ], *root;
- //文本字符串
- char msg[MAXN];
- int N;
- void TrieInsert(char *s)
- {
- int i = 0;
- TrieNode *ptr = root;
- while(s[i])
- {
- int idx = s[i]-'a';
- if(ptr->next[idx] == NULL)
- ptr->next[idx] = new TrieNode();
- ptr = ptr->next[idx];
- i++;
- }
- ptr->danger = true;
- ptr->cnt++;
- }
- void Init()
- {
- int i;
- char s[100];
- root = new TrieNode();
- scanf("%d", &N);
- for(i = 0; i < N; i++)
- {
- scanf("%s", s);
- TrieInsert(s);
- }
- }
- void Build_AC_Automation()
- {
- int rear = 1, front = 0, i;
- que[0] = root;
- root->fail = NULL;
- while(rear != front)
- {
- TrieNode *cur = que[front++];
- for(i = 0; i < 26; i++)
- if(cur->next[i] != NULL)
- {
- if(cur == root)
- cur->next[i]->fail = root;
- else
- {
- TrieNode *ptr = cur->fail;
- while(ptr != NULL)
- {
- if(ptr->next[i] != NULL)
- {
- cur->next[i]->fail = ptr->next[i];
- if(ptr->next[i]->danger == true)
- cur->next[i]->danger = true;
- break;
- }
- ptr = ptr->fail;
- }
- if(ptr == NULL) cur->next[i]->fail = root;
- }
- que[rear++] = cur->next[i];
- }
- }
- }
- int AC_Search()
- {
- int i = 0, ans = 0;
- TrieNode *ptr = root;
- while(msg[i])
- {
- int idx = msg[i]-'a';
- while(ptr->next[idx] == NULL && ptr != root) ptr = ptr->fail;
- ptr = ptr->next[idx];
- if(ptr == NULL) ptr = root;
- TrieNode *tmp = ptr;
- while(tmp != NULL && tmp->cnt != -1)
- {
- ans += tmp->cnt;
- tmp->cnt = -1;
- tmp = tmp->fail;
- }
- i++;
- }
- return ans;
- }
- int main()
- {
- int T;
- scanf("%d", &T);
- while(T--)
- {
- Init();
- Build_AC_Automation();
- //文本
- scanf("%s", msg);
- printf("%d\n", AC_Search());
- }
- return 0;
- }