字符串算法—字符串搜索


1. 前文回顧

  在字符串算法—字典樹(Tries)中,我們實現了在一堆字符串中尋找某個字符串的高效算法。但如果要從一段字符中,尋找某個字符串呢?

  我們可以用字符串算法—字符串排序(下篇)中的后綴排序法(suffix arrays)來尋找關鍵詞,但它消耗的內存有點大(畢竟要建一個超大的數組)。

  為了解決這個問題,本文將介紹KMP算法(Knuth-Morris-Pratt)和BM算法(Boyer-Moore)。

 

2. KMP算法(Knuth-Morris-Pratt)

  簡單介紹一下問題:

  現有一段字符:AABACAABABACAA

  問:ABABAC是否在這段字符里,如果在,在哪里?

  從解決這個問題的過程中了解KMP算法:

  

  左上角那個表格是KMP算法要用到的輔助表格,最下面的那個圖是這個表格的圖像化(方便理解用),右上角的圖為題目中的那段字符。

  輔助表格如何建立,我們等下再介紹,我們先直接用。輔助表格中的字符是題目要我們找的字符串。

  為了方便講解,我們命名題目中的那段字符為字符串S;要尋找的字符為字符串G;即:

  字符串S: AABACAABABACAA

  字符串G: ABABAC

  首先,我們對比S和G的第一個字符,處於輔助表格的第0階段:

  

  由於字符串S只有A,B,C三種字母,所以輔助表格只考慮了A,B,C三種情況。

  處於第0階段時,如果遇見的是A,則前往第1階段;如果遇見的是B或者C,則停留在第0階段;

  我們這里遇見的是S的第一個字符A,故前往第1階段:

  

  然后我們來看S的下一個字符A,

  處於第1階段時,如果遇見的是A,則停留在第1階段;如果遇見的是B,則前往第2階段;如果遇見的是C,則前往第0階段;

  現在,我們遇到的是A,故停留在第1階段:

  

  然后我們來看S的下一個字符B,處於第1階段遇見B,前往第2階段:(如果有字符已匹配上,則用綠色表示)

  

  處於第2階段時,如果遇見的是A,則前往第3階段;如果遇見的是B或C,則前往第0階段

  我們來看S的下一個字符A,故前往第3階段:

  

  處於第3階段時,如果遇見的是A,則前往第1階段;如果遇見的是B,則前往第4階段;如果遇見的是C,則前往第0階段;

  我們來看S的下一個字符C,故前往第0階段:

  

  處於第0階段時,如果遇見的是A,則前往第1階段;如果遇見的是B或者C,則停留在第0階段;

  我們這里遇見的是S的下一個字符A,故前往第1階段:

  

  處於第1階段時,如果遇見的是A,則停留在第1階段;如果遇見的是B,則前往第2階段;如果遇見的是C,則前往第0階段;

  現在,我們遇到的是S的下一個字符A,故停留在第1階段:

  

  處於第1階段時,如果遇見的是A,則停留在第1階段;如果遇見的是B,則前往第2階段;如果遇見的是C,則前往第0階段;

  我們遇到的是S的下一個字符B,故前往第2階段:

  

  處於第2階段時,如果遇見的是A,則前往第3階段;如果遇見的是B或C,則前往第0階段

  我們來看S的下一個字符A,故前往第3階段:

  

  處於第3階段時,如果遇見的是A,則前往第1階段;如果遇見的是B,則前往第4階段;如果遇見的是C,則前往第0階段;

  我們來看S的下一個字符B,故前往第4階段:

  

  處於第4階段時,如果遇見的是A,則前往第5階段;如果遇見的是B或C,則前往第0階段

  我們來看S的下一個字符A,故前往第5階段:

  

  處於第5階段時,如果遇見的是A,則前往第1階段;如果遇見的是B,則前往第4階段;如果遇見的是C,則前往第6階段;

  我們來看S的下一個字符C,故前往第6階段:

  

  第6階段就是最終階段了,來到這個階段,說明已經找到字符串G了,至於G在字符串S的什么位置,這個容易求:

  由於我們是逐個檢查字符串S的,(從int i=0開始逐漸遞增)所以我們是知道正在檢查第幾個字符的(i)。

  int T= i-字符串G的長度+1; T就是字符串G所處位置,在本例子中,T=6,即字符串G在字符串S的第6個字符處。

  如果我們需要知道某個字符串在某段字符中出現過多少次,分別在哪,則可以在每次找到此字符串時,重新回到第0階段,繼續尋找下去。

  在本例中,任務完成了,算法結束。

  順帶一提:所謂的第幾階段就是有幾個字符已經匹配上了。例如處於第三階段時,字符串G和字符串S已經匹配上了3個字符。

  現在開始討論如何建立輔助表格:

  首先最簡單的就是每個階段都遇到了正確的字符,即:

  我們要查找的字符串G為ABABAC,第0階段遇到A;第1階段遇到B;第2階段遇到A;第3階段遇到B;第4階段遇到A;第5階段遇到C;那么每個階段都會前往下一個階段:

  

  當我們遇到的是不正確的字符,該怎么辦呢?這里新增一個整數型變量int X=0;這個X將起輔助作用。

  首先第0階段,只有遇到正確的字符才會前進,否則停留在原地,故:

  

  然后到第1階段,當我們在第X階段時(X=0),遇到A會前往第1階段;遇到C會停留在第0階段。把這個結果填進第1階段:(圖中標紅的是第X階段)

  

  然后更新X:現在在第1階段,第1階段的字符為B,在第X階段(X=0)遇到B會停留在第0階段,故X=0,X值沒改變。

  然后到第2階段,當我們在第X階段時(X=0),遇到B會停留在第0階段;遇到C會停留在第0階段。把這個結果填進第2階段:

  

  然后更新X:現在在第2階段,第2階段的字符為A,在第X階段(X=0)遇到A會前往第1階段,故X=1。

  

  然后到第3階段,當我們在第X階段時(X=1),遇到A會前往第1階段;遇到C會前往第0階段。把這個結果填進第3階段:

  

  然后更新X:現在在第3階段,第3階段的字符為B,在第X階段(X=1)遇到B會前往第2階段,故X=2。

  

  然后到第4階段,當我們在第X階段時(X=2),遇到B會前往第0階段;遇到C會前往第0階段。把這個結果填進第4階段:

  

  然后更新X:現在在第4階段,第4階段的字符為A,在第X階段(X=2)遇到A會前往第3階段,故X=3。

  

  然后到第5階段,當我們在第X階段時(X=3),遇到A會前往第1階段;遇到B會前往第4階段。把這個結果填進第5階段:

  

  這樣輔助表格就做好了。

  輔助表格的制作過程加上一開始介紹的尋找過程就是完整的KMP算法了。

實現代碼

  建立表格:

  

  尋找字符:

  

 

3. KMP算法效率

  

  

  Brute force(暴風算法)是種蠻力算法,它把要查找的字符串與原字符串的第一個字符開始一一對比,如果發現不匹配的字符,則從下一個字符開始一一個對比。如此類推,直到找到了該字符串或原字符串所有字符都對比完(找不到該字符串的情況)為止。

  由於此算法效率低下,這里沒有細講。

  圖中N為原字符串所含字符數量;M為要找的字符串所含字符數量,R為字符串中可能出現的字符種類數量,根據下圖選擇:

  

 

4. BM算法(Boyer-Moore)

  百度了一下:在用於查找子字符串的算法當中,BM(Boyer-Moore)算法是目前被認為最高效的字符串搜索算法,它由Bob Boyer和J Strother Moore設計於1977年。 一般情況下,比KMP算法快3-5倍。

  這個算法牛的地方在於要查找的字符串越長,搜索效率越高。

  從例子入手:

  

  

  如上圖,我們把所有的字符的值定為-1,要查找的字符串needle含有4個不同的字符,我們根據它們所處位置給它們賦值:right[N]=0; right[E]=5; right[D]=3; right[L]=4;其中E出現了3次,我們取其中的最大值。

  新增整數變量 int i=0; int j=0; 原字符串長度N=24; 查找的字符串長度M=6;

  首先。j=M-1;即j=5;從第j個字符開始比較:

  

  比較結果:不相等。

  然后要決定我們可以跳幾個字符:原字符串的第5個字符為N,right[N]=0; j-right[N]=5-0=5; 故我們可以跳5個字符:i +=5; i=5;

  

  j+i=5+5=10;比較原字符的第10個字符:

  

  比較結果:不相等。

  然后要決定我們可以跳幾個字符:原字符串的第10個字符為S,right[S]=-1; j-right[S]=5-(-1)=6; 故我們可以跳6個字符(因為s不在要查找的字符串里,所以可以把這整段跳過去):i +=6; i=11;

  

  j+i=5+11=16;比較原字符的第16個字符:

  

  比較結果:相等。比較前一個字符, j--; j=5-1=4:

  

  計較結果:相等。

  然后要決定我們可以跳幾個字符:原字符串的第15個字符為N,right[N]=0; j-right[N]=4-0=4; 故我們可以跳4個字符:i +=4; i=15, j=M; j=5;

  

  j+i=5+15=20;比較原字符的第20個字符:

  

  比較結果:相等。比較前一個字符, j--; j=5-1=4:

  

  比較結果:相等。比較前一個字符, j--; j=4-1=3:(由於接下來的結果都是相等,故省略中間過程,直接跳到j=0)

  

  比較結果:相等。由於j=0;再減下去就是負數了,算法也在這里結束。要查找的字符串在原字符的第i個字符處(i=15)。

  另外,值得一提的是,請看以下情況:

  

  此時,j=3, 比較結果不相同。

  然后要決定我們可以跳幾個字符:原字符串的第18個字符為E,right[E]=5; j-right[E]=3-5=-2; 跳的步數為負數,我們總不能往回跳吧!故當跳的步數為負時,我們只往前跳一個字符:  

  

  此時如果再跳,就要跳出字符串之外了,故當i>N-M時,我們停止算法,判斷結果為沒找到該字符。

  當我們的要找的字符串越長時,我們可能能跳的字符數也越多,算法越快。(為什么是可能?因為當原字符串的字符都出現在要查找的字符串時,是沒辦法跳M個字符的。)

 

實現代碼:

  

  

 

5. BM算法效率

  

  圖中N為原字符串所含字符數量;M為要找的字符串所含字符數量,R為字符串中可能出現的字符種類數量。

  但是,如果遇到最壞情況:

  

  在這種情況下,BM算法跳不了,只能一步一步往前比較。此時就等於是Brute force(暴風算法)了。


免責聲明!

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



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