String Algorithm 字符串算法专题

这个专题主要要处理的字符串匹配(String Matching Problem)strstr 问题:

假设有一个字符串Text T,长度:n,即T[0...n-1]

现在要在T中找Pattern P,长度:m,即P[0...m-1]   (n>=m)


常用的算法有:

1)暴力法 Brute Force Method

2)Rabin-Karp String Matching Algorithm

3)String Matching with Finite Automata

4)KMP Algorithm

5)Boyce-Moore Algorithm

6)后缀树 Suffix Trees


1)暴力法 Brute Force Method:



[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package String;  
  2.   
  3. public class BruteForce {  
  4.   
  5.     public static void main(String[] args) {  
  6.         String T = "mississippi";  
  7.         String P = "ssi";  
  8.         bruteForce(T, P);  
  9.     }  
  10.   
  11.     // Time: O(nm), Space: O(1)  
  12.     public static void bruteForce(String T, String P) {  
  13.         int n = T.length();     // Text  
  14.         int m = P.length();     // Pattern  
  15.           
  16.         for(int i=0; i<=n-m; i++) {      // i表示在T上的offset,注意最后一个开始检查位置是n-m  
  17.             int j = 0;  
  18.             while(j < m && T.charAt(i+j) == P.charAt(j)) {   // j表示匹配到P的哪个位置了  
  19.                 j++;  
  20.             }  
  21.             if(j == m) {        // j已经全部匹配完P的长度,返回第一个匹配开始的地点  
  22.                 System.out.println("Pattern found at index " + i);  
  23.             }  
  24.         }  
  25.     }  
  26. }  


http://www.geeksforgeeks.org/searching-for-patterns-set-1-naive-pattern-searching/


2)Rabin-Karp String Matching Algorithm

Rabin-Karp的预处理时间是O(m),匹配时间O( ( n - m + 1 ) m )既然与朴素算法的匹配时间一样,而且还多了一些预处理时间,那为什么我们还要学习这个算法呢?虽然Rain-Karp在最坏的情况下与朴素匹配一样,但是实际应用中往往比朴素算法快很多。而且该算法的期望匹配时间是O(n)【参照《算法导论》】,但是Rabin-Karp算法需要进行数值运算,速度必然不会比KMP算法快,那我们有了KMP算法以后为什么还要学习Rabin-Karp算法呢?个人认为学习的是一种思想,一种解题的思路,当我们见识的越多,眼界也就也开阔,面对实际问题的时候,就能找到更加合适的算法。比如二维模式匹配,Rabin-Karp就是一种好的选择。
      而且Rabin-Karp算法非常有趣,将字符当作数字来处理,基本思路:如果Tm是一个长度为 |P| 的T的子串,且转换为数值后模上一个数(一般为素数)与模式字符串P转换成数值后模上同一个数的值相同,则Tm可能是一个合法的匹配。

该算法的难点就在于p和t的值可能很大,导致不能方便的对其进行处理。对这个问题有一个简单的补救办法,用一个合适的数q来计算p和t的模。每个字符其实十一个十进制的整数,所以p,t以及递归式都可以对模q进行,所以可以在O(m)的时间里计算出模q的p值,在O(n - m + 1)时间内计算出模q的所有t值。参见《算法导论》或http://net.pku.edu.cn/~course/cs101/2007/resource/Intro2Algorithm/book6/chap34.htm
递推式是如下这个式子: 
ts+1 = (d ( ts -T[s + 1]h) + T[s + m + 1 ] ) mod q
例如,如果d = 10 (十进制)m= 5, ts = 31415,我们希望去掉最高位数字T[s + 1] = 3,再加入一个低位数字(假定 T[s+5+1] = 2)就得到:
ts+1 = 10(31415 - 1000*3) +2 = 14152


平均,最好时间复杂度:O(n+m)

最坏时间复杂度:O(nm)

最差情况发生于所有的text的字串和pattern都有相同的哈希值,使算法退化到O(nm)

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package String;  
  2.   
  3. public class RabinKarp {  
  4.   
  5.     public static void main(String[] args) {  
  6.         String T = "mississippi";  
  7.         String P = "ssi";  
  8.         int q = 101;  
  9.         search(P, T, q);  
  10.     }  
  11.       
  12.     public static int d = 256;  
  13.       
  14.     public static void search(String P, String T, int q) {  
  15.         int M = P.length();  
  16.         int N = T.length();  
  17.         int i, j;  
  18.         int p=0;        // hash value for pattern   
  19.         int t=0;        // hash value for txt  
  20.         int h=1;  
  21.           
  22.         // The value of h would be "pow(d, M-1)%q"  
  23.         for(i=0; i<M-1; i++) {  
  24.             h = (h*d)%q;  
  25.         }  
  26.           
  27.         // Calculate the hash value of pattern and first window of text  
  28.         for(i=0; i<M; i++) {  
  29.             p = (d*p + P.charAt(i)) % q;  
  30.             t = (d*t + T.charAt(i)) % q;  
  31.         }  
  32.           
  33.         // Slide the pattern over text one by one   
  34.         for(i=0; i<=N-M; i++) {  
  35.             // Chaeck the hash values of current window of text and pattern  
  36.             // If the hash values match then only check for characters on by one  
  37.             if(p == t) {  
  38.                 // Check for characters one by one  
  39.                 for(j=0; j<M; j++) {  
  40.                     if(T.charAt(i+j) != P.charAt(j)) {  
  41.                         break;  
  42.                     }  
  43.                 }  
  44.                 if(j == M) {        // if p == t and pat[0...M-1] = txt[i, i+1, ...i+M-1]  
  45.                     System.out.println("Pattern found at index " + i);  
  46.                 }  
  47.             }  
  48.               
  49.             // Calulate hash value for next window of text: Remove leading digit,   
  50.             // add trailing digit  
  51.             if(i < N-M) {  
  52.                 // Rehash, O(1)  
  53.                 t = ( d*(t - T.charAt(i)*h) + T.charAt(i+M) ) % q;  
  54.                 // We might get negative value of t, converting it to positive  
  55.                 if(t < 0) {  
  56.                     t = t + q;  
  57.                 }  
  58.             }  
  59.         }  
  60.     }  
  61.   
  62. }  

http://www.geeksforgeeks.org/searching-for-patterns-set-3-rabin-karp-algorithm/

http://www.youtube.com/watch?v=d3TZpfnpJZ0


3)String Matching with Finite Automata

假设要对文本字符串T进行扫描,找出模式P的所有出现位置。这个方法可以通过一些办法先对模式P进行预处理,然后只需要对T的每个文本字符检查一次,并且检查每个文本字符所用时间为常数,所以在预处理建好自动机之后进行匹配所需时间只是Θ(n)。

  假设文本长度为n,模式长度为m,则自动机将会有0,1,...,m这么多种状态,并且初始状态为0。先抛开自动机是怎样计算出来的细节,只关注自动机的作用。在从文本从左到右扫描时,对于每一个字符a,根据自动机当前的状态还有a的值可以找出自动机的下一个状态,这样一直扫描下去,并且一定自动机状态值变为m的时候我们就可以认为成功进行了一次匹配。先看下面简单的例子:

 

假设现在文本和模式只有三种字符a,b,c,已经文本T为"abababaca",模式P为"ababaca",根据模式P建立自动机如下图(b)(先不管实现细节):

 (a)图为一些状态转化细节

 




 

 

 

如图(c),对照自动机转换图(b),一个个的扫描文本字符,扫描前状态值初始化为0,这样在i = 9的时候状态值刚好变成7 = m,所以完成一个匹配。

 

  现在问题只剩下怎样根据给出的模式P计算出相应的一个自动机了。这个过程实际上并没有那么困难,下面只是介绍自动机的构建,而详细的证明过程可以参考书本。

  还是用上面的那里例子,建立模式P = "ababaca"的有限自动机。首先需要明白一点,如果当前的状态值为k,其实说明当前文本的后缀与模式的前缀的最大匹配长度为k,这时读进下一个文本字符,即使该字符匹配,状态值最多变成k + 1.假设当前状态值为5,说明文本当前位置的最后5位为"ababa",等于模式的前5位。

  如果下一位文本字符是"c",则状态值就可以更新为6.如果下一位是"a",这时我们需要重新找到文本后缀与模式前缀的最大匹配长度。简单的寻找方法可以是令k = 6(状态值最大的情况),判断文本后k位与模式前k位是否相等,不等的话就k = k - 1继续找。由于刚才文本后5位"ababa"其实就是模式的前5位,所以实际上构建自动机时不需要用到文本。这样可以找到这种情况状态值将变为1(只有a一位相等)。同理可以算出下一位是"b"时状态值该变为4(模式前4位"abab"等于"ababab"的后缀)

  下面是书本伪代码:∑代表字符集,δ(q,a)可以理解为读到加进字符a后的状态值


  用上面的方法计算自动机,如果字符种数为k,则建立自动机预处理的时间是O(m ^ 3 * k),有方法可以将时间改进为O(m * k)。预处理完后需要Θ(n)的处理时间。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package String;  
  2.   
  3. public class FiniteAutomata {  
  4.   
  5.     public static int getNextState(String pat, int M, int state, int x) {  
  6.         // If the character c is same as next character in pattern,  
  7.         // then simply increment state  
  8.         if(state < M && x == pat.charAt(state)) {  
  9.             return state + 1;  
  10.         }  
  11.           
  12.         int ns, i;      // ns stores the result which is next state  
  13.           
  14.         // ns finally contains the longest prefix which is also suffix  
  15.         // in "pat[0..state-1]c"  
  16.        
  17.         // Start from the largest possible value and stop when you find  
  18.         // a prefix which is also suffix  
  19.         for(ns = state; ns > 0; ns--) {  
  20.             if(pat.charAt(ns-1) == x) {  
  21.                 for(i=0; i<ns-1; i++) {  
  22.                     if(pat.charAt(i) != pat.charAt(state-ns+1+i)) {  
  23.                         break;  
  24.                     }  
  25.                 }  
  26.                 if(i == ns-1) {  
  27.                     return ns;  
  28.                 }  
  29.             }  
  30.         }  
  31.         return 0;  
  32.     }  
  33.       
  34.     public static int NO_OF_CHARS = 256;  
  35.       
  36.     /* This function builds the TF table which represents Finite Automata for a 
  37.        given pattern  */  
  38.     public static void computeTF(String pat, int M, int[][] TF) {  
  39.         int state, x;  
  40.         for(state=0; state<=M; state++) {  
  41.             for(x=0; x<NO_OF_CHARS; x++) {  
  42.                 TF[state][x] = getNextState(pat, M, state, x);  
  43.             }  
  44.         }  
  45.     }  
  46.       
  47.     /* Prints all occurrences of pat in txt */  
  48.     public static void search(String pat, String txt) {  
  49.         int M = pat.length();  
  50.         int N = txt.length();  
  51.           
  52.         int[][] TF = new int[M+1][NO_OF_CHARS];  
  53.           
  54.         computeTF(pat, M, TF);  
  55.           
  56.         // Process txt over FA.  
  57.         int i, state = 0;  
  58.         for(i=0; i<N; i++) {  
  59.             state = TF[state][txt.charAt(i)];  
  60.             if(state == M) {  
  61.                 System.out.println("Pattern found at index " +(i-M+1));  
  62.             }  
  63.         }  
  64.     }  
  65.       
  66.     public static void main(String[] args) {  
  67.         String T = "mississippi";  
  68.         String P = "ssi";  
  69.         search(P, T);  
  70.     }  
  71. }  

http://www.cnblogs.com/jolin123/p/3443543.html

http://www.geeksforgeeks.org/searching-for-patterns-set-5-finite-automata/


4)KMP Algorithm

KMP算法,最易懂的莫过于Princeton的Robert Sedgewick讲解的,他用自动机模型来讲解。难点在于建立dfa表,精妙处在于维护一个X的变量,每次基于match和mismatch两种情况,再结合X位置的情况,来推出当前位置的情况,而且更新X的值。

有了dfa表后,search就变成了线性的过程,要点是i指针一直向前走,从来不往后退。j表示不同的状态,也是match上字符的个数。

Time Complexity: O(n+m)

Space Complexity: O(m)

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package String;  
  2.   
  3. public class KMP {  
  4.   
  5.     private static int[][] dfa;  
  6.       
  7.     // return offset of first match; N if no match  
  8.     public static int search(String text, String pat) {  
  9.           
  10.         createDFA(pat);  
  11.           
  12.         // simulate operation of DFA on text  
  13.         int M = pat.length();  
  14.         int N = text.length();  
  15.           
  16.         int i, j;  
  17.         for(i=0, j=0; i<N && j<M; i++) {  
  18.             j = dfa[text.charAt(i)][j];  
  19.         }  
  20.         if(j == M) {        // found  
  21.             System.out.println("Find pattern at index: " + (i-M));  
  22.             return i - M;  
  23.         }  
  24.         System.out.println("Not found");  
  25.         return N;       // not found  
  26.     }  
  27.       
  28.     // create the DFA from a String  
  29.     public static void createDFA(String pat) {  
  30.         int R = 256;  
  31.         int M = pat.length();       // build DFA from pattern  
  32.         dfa = new int[R][M];  
  33.         dfa[pat.charAt(0)][0] = 1;  
  34.         for(int X=0, j=1; j<M; j++) {  
  35.             for(int c=0; c<R; c++) {  
  36.                 dfa[c][j] = dfa[c][X];      // Copy mismatch cases.   
  37.             }  
  38.             dfa[pat.charAt(j)][j] = j+1;    // Set match case.   
  39.             X = dfa[pat.charAt(j)][X];      // Update restart state.   
  40.         }  
  41.     }  
  42.       
  43.       
  44.     // ======================================= char array  
  45.       
  46.     // return offset of first match; N if no match  
  47.     public static int search(char[] text, char[] pattern) {  
  48.           
  49.         createDFA(pattern, 256);  
  50.           
  51.         int M = pattern.length;  
  52.         int N = text.length;  
  53.         int i, j;  
  54.         for(i=0, j=0; i<N && j<M; i++) {  
  55.             j = dfa[text[i]][j];  
  56.         }  
  57.         if(j == M) {        // found  
  58.             System.out.println("Find pattern at index: " + (i-M));  
  59.             return i - M;  
  60.         }  
  61.         System.out.println("Not found");  
  62.         return N;       // not found  
  63.     }  
  64.       
  65.     // create the DFA from a character array over R-character alphabet  
  66.     public static void createDFA(char[] pattern, int R) {  
  67.         int M = pattern.length;  
  68.         dfa = new int[R][M];  
  69.           
  70.         dfa[pattern[0]][0] = 1;  
  71.         for(int X=0, j=1; j<M; j++) {  
  72.             for(int c=0; c<R; c++) {  
  73.                 dfa[c][j] = dfa[c][X];  
  74.             }  
  75.             dfa[pattern[j]][j] = j+1;  
  76.             X = dfa[pattern[j]][X];  
  77.         }  
  78.     }  
  79.       
  80.     public static void main(String[] args) {  
  81.         String T = "mississippi";  
  82.         String P = "ssi";  
  83.         search(T, P);  
  84.         search(T.toCharArray(), P.toCharArray());  
  85.     }  
  86.   
  87. }  

http://algs4.cs.princeton.edu/53substring/KMP.java.html

https://www.cs.princeton.edu/courses/archive/fall10/cos226/demo/53KnuthMorrisPratt.pdf

http://www.cmi.ac.in/~kshitij/talks/kmp-talk/kmp.pdf




5)Boyce-Moore Algorithm

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. package String;  
  2.   
  3. public class BoyerMoore {  
  4.   
  5.     private final int R;     // the radix  
  6.     private int[] right;     // the bad-character skip array  
  7.   
  8.     private char[] pattern;  // store the pattern as a character array  
  9.     private String pat;      // or as a string  
  10.   
  11.     // pattern provided as a string  
  12.     public BoyerMoore(String pat) {  
  13.         this.R = 256;  
  14.         this.pat = pat;  
  15.   
  16.         // position of rightmost occurrence of c in the pattern  
  17.         right = new int[R];  
  18.         for (int c = 0; c < R; c++)  
  19.             right[c] = -1;  
  20.         for (int j = 0; j < pat.length(); j++)  
  21.             right[pat.charAt(j)] = j;  
  22.     }  
  23.   
  24.     // pattern provided as a character array  
  25.     public BoyerMoore(char[] pattern, int R) {  
  26.         this.R = R;  
  27.         this.pattern = new char[pattern.length];  
  28.         for (int j = 0; j < pattern.length; j++)  
  29.             this.pattern[j] = pattern[j];  
  30.   
  31.         // position of rightmost occurrence of c in the pattern  
  32.         right = new int[R];  
  33.         for (int c = 0; c < R; c++)  
  34.             right[c] = -1;  
  35.         for (int j = 0; j < pattern.length; j++)  
  36.             right[pattern[j]] = j;  
  37.     }  
  38.   
  39.     // return offset of first match; N if no match  
  40.     public int search(String txt) {  
  41.         int M = pat.length();  
  42.         int N = txt.length();  
  43.         int skip;  
  44.         for (int i = 0; i <= N - M; i += skip) {  
  45.             skip = 0;  
  46.             for (int j = M-1; j >= 0; j--) {  
  47.                 if (pat.charAt(j) != txt.charAt(i+j)) {  
  48.                     skip = Math.max(1, j - right[txt.charAt(i+j)]);  
  49.                     break;  
  50.                 }  
  51.             }  
  52.             if (skip == 0return i;    // found  
  53.         }  
  54.         return N;                       // not found  
  55.     }  
  56.   
  57.   
  58.     // return offset of first match; N if no match  
  59.     public int search(char[] text) {  
  60.         int M = pattern.length;  
  61.         int N = text.length;  
  62.         int skip;  
  63.         for (int i = 0; i <= N - M; i += skip) {  
  64.             skip = 0;  
  65.             for (int j = M-1; j >= 0; j--) {  
  66.                 if (pattern[j] != text[i+j]) {  
  67.                     skip = Math.max(1, j - right[text[i+j]]);  
  68.                     break;  
  69.                 }  
  70.             }  
  71.             if (skip == 0return i;    // found  
  72.         }  
  73.         return N;                       // not found  
  74.     }  
  75.   
  76.   
  77.   
  78.     // test client  
  79.     public static void main(String[] args) {  
  80.         String pat = "ssi";  
  81.         String txt = "mississippi";  
  82.         char[] pattern = pat.toCharArray();  
  83.         char[] text    = txt.toCharArray();  
  84.   
  85.         BoyerMoore boyermoore1 = new BoyerMoore(pat);  
  86.         BoyerMoore boyermoore2 = new BoyerMoore(pattern, 256);  
  87.         int offset1 = boyermoore1.search(txt);  
  88.         int offset2 = boyermoore2.search(text);  
  89.   
  90.         System.out.println("Find in offset: " + offset1);  
  91.         System.out.println("Find in offset: " + offset2);  
  92.     }  
  93. }  

https://www.youtube.com/watch?v=rDPuaNw9_Eo

http://algs4.cs.princeton.edu/53substring/BoyerMoore.java.html


6)后缀树 Suffix Trees

在写后缀树之前,还得先介绍两种也很常用的存储String的数据结构:Trie和Ternary Search Tree

Trie的总结可参考这里:http://blog.csdn.net/fightforyourdream/article/details/18332799

Trie的优点在于查找速度很快,缺点在于内存需要很多,因为每个节点都要存26个指针,指向其孩子。

因此Ternary Search Tree应运而生。它结合了BST的内存高效和Trie的时间高效。

具体Ternary Search Tree的解释可以参考:

http://www.cnblogs.com/rush/archive/2012/12/30/2839996.html

http://www.geeksforgeeks.org/ternary-search-tree/

举个例子:

把字符串AB,ABCD,ABBA和BCD插入到三叉搜索树中,首先往树中插入了字符串AB,接着我们插入字符串ABCD,由于ABCD与AB有相同的前缀AB,所以C节点都是存储到B的CenterChild中,D存储到C的CenterChild中;当插入ABBA时,由于ABBA与AB有相同的前缀AB,而B字符少于字符C,所以B存储到C的LeftChild中;当插入BCD时,由于字符B大于字符A,所以B存储到C的RightChild中。

trie5


其实还可以用Hashtable来存放字符串,它内存高效,但是无法排序。


后缀树视频:

https://www.youtube.com/watch?v=hLsrPsFHPcQ

v_JULY_v的从Trie树(字典树)谈到后缀树(10.28修订)http://blog.csdn.net/v_july_v/article/details/6897097


后缀树组

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值