posts - 73,  comments - 55,  trackbacks - 0

          1 術語定義

          在字符串匹配問題中,我們期待察看串T中是否含有串P。
          其中串T被稱為目標串,串S被稱為模式串。

          2 樸素匹配算法

          進行字符串匹配,最簡單的一個想法是:

          public ? class ?SimpleMatch? {
          ??
          public ? int ?StringMatch(String?target,String?patten)? {
          ??????
          int ?tl? = ?target.length();
          ??????
          int ?pl? = ?patten.length();
          ??????
          int ?i? = ? 0 ;
          ??????
          int ?j? = ? 0 ;
          ??????
          while (i? < ?tl? - ?pl? && ?j? < ?pl)? {
          ??????????
          if (patten.charAt(j)? == ?target.charAt(i + j))
          ??????????????j
          ++ ;
          ??????????
          else ? {
          ??????????????j?
          = ? 0 ;
          ??????????????i
          ++ ;
          ??????????}

          ??????}

          ??????
          if (j? == ?pl)
          ??????????
          return ?i;
          ??????
          return ? - 1 ;
          ??}

          ??
          ??
          public ? static ? void ?main(String[]?args) {
          ??????String?t?
          = ? " 123456789 " ;
          ??????String?p?
          = ? " 456 " ;
          ??????SimpleMatch?sm?
          = ? new ?SimpleMatch();
          ??????System.out.println(sm.StringMatch(t,?p));
          ??}

          }

          可以看見,這個算法(假定m>>n)的復雜度是O(mn),其中m是T的長度,n是P的長度。這種算法的缺陷是匹配過程中帶有回溯——準確地說是T串存在回溯,也就是當匹配不成功的時候,之前進行的匹配完全變為無用功,所有的比較需要重新開始。

          3 KMP算法

          KMP算法是D.E.Knuth、J.H.Morris和V.R.Pratt提出的無回溯的字符串匹配算法,算法的核心思想就是設法在匹配失敗的時候,盡量利用之前的匹配結果,消除T串的回溯問題。那么如何消除回溯呢?請看下面的例子:

          假設P=abacd,如果T=abax...,當從頭開始匹配到字符c時,若c=x,顯然,匹配過程繼續;當c≠x時,按照樸素的匹配算法,T串會發生回溯,之后T串會從第2個字符b開始重新匹配,而不是從匹配失敗的字符x開始繼續。但是顯然,對于上述的匹配過程,T串不需要從b開始重新匹配,它只需要從x開始和P的b字符繼續匹配即可。如下:
          匹配過程:
          P=abacd
          T=abax....
          ???? ^----比較到此處時發生匹配失敗
          樸素匹配算法:
          P= abacd
          T=abax...
          ?? ^----回溯到b,重新開始和P的匹配
          KMP算法:
          P=? abacd
          T=abax...
          ???? ^----T串不回溯,從x處繼續匹配

          現在的問題是,按照KMP算法,匹配失敗的時候,P串需要重新調整位置,但是調整的依據是什么?Knuth等人發現,P調整位置的依據和P的構造有關,和T無關。具體來說,定義失效函數:f(j)=k,其中0<=k<=j,且k是使得p0p1...pk-1 = pj-k+1pj-k+2...pj成立的最大整數。建立失效函數的算法如下:
          public void Build() {
          ?if(pattern == null)
          ??throw new Exception("KMP Exception : null pattern");
          ?array = new int[pattern.Length];
          ?int i = 0, s = pattern.Length;
          ?if(s > 1)
          ??array[0] = 0;
          ?for(i = 1; i < s; i++) {
          ??if(pattern[i] == pattern[array[i - 1]])
          ???array[i] = array[i - 1] + 1;
          ??else
          ???array[i] = 0;
          ?}
          }

          匹配過程如下:
          public int Match(String target, int start) {
          ?if(array == null || pattern == null || target == null)
          ??return -1;
          ?int target_index = start;
          ?int pattern_index = 0;
          ?int token_length = target.Length;
          ?int pattern_length = pattern.Length;
          ?while(target_index < token_length && pattern_index < pattern_length) {
          ??if(target[target_index] == pattern[pattern_index]) {
          ???target_index++;
          ???pattern_index++;
          ??} else {
          ???if(pattern_index == begin)
          ????target_index++;
          ???else
          ????pattern_index = array[pattern_index - 1];
          ??}
          ?}
          ?if(pattern_index == pattern_length)
          ??return target_index - pattern_length;
          ?return -1;
          }

          4 支持通配符?和*的KMP算法

          KMP算法雖然能夠進行字符串匹配,但是,在實踐中字符串匹配往往還要支持通配符,MS系統中最常見的通配符是?和*。其中,?可以代表一個字符(不能沒有),*可以代表任意多個字符(可以為空)。經典的KMP算法針對通配符是無能為力的,但是經過簡單的改造,KMP算法也可以識別通配符。

          首先是?,根據?的功能,?表示任意字符,也就是說在匹配過程中,?永遠匹配成功。因此對匹配函數的修改十分簡單:
          ...
          ?while(target_index < token_length && pattern_index < pattern_length) {
          ??if(target[target_index] == pattern[pattern_index]|| pattern[pattern_index] == '?') {
          ???target_index++;
          ???pattern_index++;
          ??} else {
          ...
          建立失效函數的過程和匹配過程類似,修改如下:
          ...
          ?for(i = 1; i < s; i++) {
          ??if(pattern[i] == pattern[array[i - 1]]|| pattern[i] == '?' || pattern[array[i - 1]] == '?')
          ???array[i] = array[i - 1] + 1;
          ...

          本質上,?并沒有修改算法,而僅僅修改了匹配規則——遇到?則一定匹配。然而*與此不同,*的作用是匹配任意多個字符,顯然我們不能簡單的修改匹配過程而滿足要求。如果我們重新思考*的作用,我們會發現*的另一個作用就是分割P串,即如果P=P1*P2,那么與其說*代表匹配任意多個字符,不如說P的匹配條件是在匹配P1子串后再匹配P2子串。

          現在回顧失效函數的作用,如果當匹配到P的j+1位時匹配失敗,那么重新開始匹配的時候,P串的位置調整到f(j)位,直到P串的位置調整到0,則匹配重新開始。但當P=P1*P2,假如P1已經匹配成功,而在P2中發生匹配失敗,那么P串要需要調整位置,但P串無論如何調整,此時也不應該調整到0,最多調整到P2的開始處,因為P1已經匹配,只需匹配P2即可。假如P=abcab*abcab,失效函數應該是(注意之前提到*的作用):
          a b c a b * a b c a b
          0 0 0 1 2 - 6 6 6 7 8

          因此,要想讓KMP支持*,那么關鍵是要重新設計失效函數的建立算法,如下:
          public void Build() {
          ?if(pattern == null)
          ??throw new Exception("KMP Exception : null pattern");
          ?array = new int[pattern.Length];
          ?int i = 0, s = pattern.Length;
          ?if(s > 1)
          ??array[0] = 0;
          ?int begin = 0;
          ?for(i = 1; i < s; i++) {
          ??if(pattern[i] == '*') {
          ???array[i] = i;
          ???begin = i + 1;
          ??} else if(pattern[i] == pattern[array[i - 1]] || pattern[i] == '?' || pattern[array[i - 1]] == '?')
          ???array[i] = array[i - 1] + 1;
          ??else
          ???array[i] = begin;
          ?}
          }?

          算法中begin表示每段字符串的開始位置。此外,匹配過程也應該進行相應的修改,因為字符*對于匹配沒有任何幫助,它屬于占位符,因此需要跳過,匹配算法如下:
          public int Match(String target, int start) {
          ?if(array == null || pattern == null || target == null)
          ??return -1;
          ?int target_index = start;
          ?int pattern_index = 0;
          ?int token_length = target.Length;
          ?int pattern_length = pattern.Length;
          ?int begin = 0;
          ?while(target_index < token_length && pattern_index < pattern_length) {
          ??if(pattern[pattern_index] == '*') {
          ???begin = pattern_index + 1;
          ???pattern_index++;
          ??} else if(target[target_index] == pattern[pattern_index] || pattern[pattern_index] == '?') {
          ???target_index++;
          ???pattern_index++;
          ??} else {
          ???if(pattern_index == begin)
          ????target_index++;
          ???else
          ????pattern_index = array[pattern_index - 1];
          ??}
          ?}
          ?if(pattern_index == pattern_length)
          ??return target_index - pattern_length + begin;
          ?return -1;
          }

          5 正則語言和確定狀態自動機

          一個數字邏輯的問題:設計一個識別11011的電路,解這個問題的關鍵就是設計出這個電路的DFA,如下:

          仔細看看這個狀態機,是不是和KMP的算法有幾分類似呢?這并不是巧合,因為KMP算法中的失效函數總可以等價的轉化為一個DFA。當然KMP的DFA遠比識別11011的DFA要復雜,原因在于KMP接受的輸入是全體字符集合,識別11011的DFA只接受0和1這兩個輸入。我們知道,一個正則語言和一個DFA是等價的,而KMP計算失效函數的算法,實際上等價于求DFA的過程,f(j)的值實際上表明狀態j+1接受到不正確的字符時應該回溯到的狀態(注意此時輸入流并沒有前進)。普通的字符串都能看成是一個正則語言,含有通配符?和*的字符串也可以等價的轉換為一個正則表達式。但是,正則語言的集合遠比KMP算法所能支持的模式集合的更大,期間原因還是剛才提過的輸入問題。試想P=p1p2...pn,當匹配到pj的時候,如果下一個輸入字符正是pj,那么狀態機進入下一個狀態,如果不是pj,那么狀態機按照實效函數的指示轉移到狀態f(j-1),也就是說KMP狀態機的每個狀態只能根據輸入是否為pj來進行轉移。而正則表達式所對應的狀態機則有所不同,如果正則語言L=l1l2...ln,假設這些都是字母,當匹配到lj位的時候,如果下一個輸入字符正是lj,那么狀態機進入下一個狀態,否則它還可以根據輸入的值進行轉移,例如lj=c1時轉換到狀態x,lj=c2時狀態轉換到y等等。

          6 結語

          字符串匹配問題是老問題了,并沒有太多新意可言,只不過雖然KMP算法十分簡單,但它的內在含義還是十分深刻的。橫向比較KMP、DFA和正則語言、正則表達式我們會發現,它們之間存在很多的關聯,而這種比較也有利于我們更好的理解這些算法,或者改進這些算法。最后說一句,試圖利用目前的框架使得KMP算法支持全部種類的通配符(對應于正則表達式就是x?、x*、x+、{m,n}等等)是不可能,而我們也不需要這么做,因為我們還有正則表達式嘛。

          posted on 2007-03-05 15:29 保爾任 閱讀(5715) 評論(2)  編輯  收藏 所屬分類: Arithmetic & Data Structure

          FeedBack:
          # re: 字符串匹配
          2007-04-07 20:12 | 阿里
          第一算法,樸素匹配算法,錯了。提示一下,少了個等號  回復  更多評論
            
          # re: 字符串匹配
          2007-04-07 20:15 | 阿里
          再說一句,算法的首字母要小寫,命名規則。  回復  更多評論
            

          <2007年3月>
          25262728123
          45678910
          11121314151617
          18192021222324
          25262728293031
          1234567

          常用鏈接

          留言簿(4)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          •  

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 慈溪市| 武邑县| 如皋市| 博客| 砀山县| 金寨县| 耿马| 杭锦旗| 兰西县| 抚远县| 罗源县| 拜城县| 通山县| 宜君县| 湖南省| 五河县| 长春市| 陆川县| 逊克县| 壤塘县| 沧州市| 江川县| 沁水县| 开封县| 安康市| 壶关县| 孟津县| 咸阳市| 乌苏市| 繁昌县| 芜湖县| 大埔县| 大竹县| 兰州市| 会宁县| 家居| 正宁县| 华亭县| 阳新县| 株洲县| 鄂伦春自治旗|