常用算法3 - 字符串查找/模式匹配算法(BF & KMP算法)
相信我們都有在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). 算法思想
基本思想:
- 將目標串T第一個字符與模式串P的第一個字符比較;
- 若相等,則比較T和P的第二個字符
- 若不等,則比較T的下一個字符與P的第一個字符
- 重復步驟以上步驟,直到匹配成功或者目標串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算法)