当前位置: 代码迷 >> 综合 >> 子字符串查找算法----暴力算法、KMP算法、Boyer-Moore算法和Rabin-Karp算法
  详细解决方案

子字符串查找算法----暴力算法、KMP算法、Boyer-Moore算法和Rabin-Karp算法

热度:35   发布时间:2023-09-21 21:33:29.0

各种算法比较

优缺点:

优点:

  • 暴力查找算法:实现简单且在一般情况下工作良好(Java的String类型的indexOf()方法就是采用暴力子字符串查找算法);
  • Knuth-Morris-Pratt算法能够保证线性级别的性能且不需要在正文中回退;
  • Boyer-Moore算法的性能一般情况下都是亚线性级别;
  • Rabin-Karp算法是线性级别;

缺点:

  • 暴力查找算法所需时间可能和NM成正比;
  • Knuth-Morris-Pratt算法和Boyer-Moore算法需要额外的内存空间;
  • Rabin-Karp算法内循环很长(若干次算术运算,其他算法都只需要比较字符);

成本总结:

算法 版本 最坏情况 一般情况 是否回退 正确性 额外空间需求
暴力算法 -- MN 1.1N 1
KMP算法 完整的DFA 2N 1.1N MR
Boyer-Moore算法 启发式查找不匹配字符 MN N/M R
Rabin-Karp算法 蒙特卡洛算法 7N 7N 是* 1
  拉斯维加斯算法 7N* 7N 1

1  暴力查找法

设文本长度为N,要匹配的模式的长度为M,暴力查找算法在最坏的情况下运行时间与MN成正比,但在处理许多应用程序中的字符串时,它的实际运行时间一般与M+N成正比。

算法实现1:

使用一个值指针i跟踪文本,一个指针j跟踪要匹配的模式,对每一个i,代码首先将j重置为0并不断增大,直到找到了一个不匹配的字符或者是匹配成功(j==M)。

public static int search(String pat, String txt) {int M = pat.length();int N = txt.length();for(int i = 0;i<= N;i++) {int j;for(j=0;j<M;j++)if(txt.charAt(i+j)!=pat.charAt(j))break;if(j==M)	return i;}return N;
}

算法实现2(显式回退):

同样使用一个值指针i跟踪文本,一个指针j跟踪要匹配的模式,在i和j指向的字符匹配时,i和j同时后移一位。如果i和j字符不匹配,那么需要回退这两个指针,j指向模式的开头,i指向这次匹配开头的下一个字符。

public static int Search(String pat,String txt) {int j, M = pat.length();int i, N = txt.length();for(i=0,j=0;i<N&&j<M;i++) {if(txt.charAt(i)==pat.charAt(j))	j++;else {i-=j;j=0;}}if(j==M)	return i-M;else	return N;
}

2  KMP算法

算法思想:

当出现不匹配时,就能知晓一部分内容(因为匹配失败之前的字符已经和模式相匹配)。可以利用这些信息避免指针回退。令人惊讶的是,KMP算法在匹配失败时,总能将j设置为一个值以使i不回退。

在KMP算法中,不会回退文本指针i,而是用一个数组dfa[][]来记录匹配失败时指针j应该回退多远。对于每一个字符c,在比较了c和pat.charAt(j)后,dfa[c][j]表示的是应该和下一个文本字符比较的模式字符的位置。在匹配时会继续比较下一个字符,因此dfa[pat.charAt(j)][i]总是j+1; 在不匹配时,不仅可以知道txt.charAt(i)的字符,还可以知道正文中前j-1个字符--它们就是模式中的前j-1个字符。

代码实现:

public class KMP {private final int R;     private int[][] dfa;          private String pat;        //构造函数中生成dfa[][]数组public KMP(String pat) {this.R = 256;this.pat = pat;int m = pat.length();dfa = new int[R][m]; dfa[pat.charAt(0)][0] = 1; for (int x = 0, j = 1; j < m; j++) {for (int c = 0; c < R; c++) dfa[c][j] = dfa[c][x];    dfa[pat.charAt(j)][j] = j+1;    x = dfa[pat.charAt(j)][x];     } } //search()方法通过dfa[][]中计算的值来查找子字符串public int search(String txt) {int m = pat.length();int n = txt.length();int i, j;for (i = 0, j = 0; i < n && j < m; i++) {j = dfa[txt.charAt(i)][j];}if (j == m) return i - m; return n;                   }
}

对于长度为M的模式字符串和长度为N的文本,KMP子字符串查找算法访问的字符串不会超过M+N个。

3  Boyer-Moore算法

Boyer-Moore算法是一种从右向左扫描模式字符串并将它与文本匹配的算法。

算法思想:

有文本FINDINAHAYSTACKNEEDLE和模式字符串NEEDLE. 因为是从右向左扫描,所以会先比较模式中最后一位E和文本中下标为5的N。不匹配,因为模式字符串中也出现了N,则右移模式字符串使得模式中最右边的N(这里是位置0的N)与文本中的相应N对齐。然后接着比较模式字符串最后的E和文本中的S(下标10),不匹配,而且模式中不含有字符S,可以将模式直接右移6位,然后继续匹配......

上述方法被称为启发式的处理不匹配字符。要实现之,需要一个数组right[]保存字母表中每个字母在模式字符串中出现的最靠右的下标(如果不存在则为-1)。这个值揭示了如果发生不匹配,应该右跳跃多远。

在right[]数组计算后,算法实现起来就非常容易了。用一个索引i在文本中从左向右移动,用索引j在模式字符串中从右向左移动。内循环检查检查正文和模式字符串在位置i是否相等,如果从M-1到0的所有j,txt.charAt(i+j)都和pat.charAt(j)相等,就是找到了匹配。否则匹配失败,失败有三种情况:

  1. 如果造成失败的字符不包含在模式字符串中,则将模式字符串向右移动j+1个位置;
  2. 如果造成失败的字符包含在模式字符串中,根据right[]数组右移模式字符串;
  3. 如果这种方法无法增大i,就直接将i+1保证模式字符串至少向右移动一个位置。

在一般情况下,对于长度为N的文本和长度为M的模式字符串,该方法通过启发式处理不匹配的字符需要~N/M次比较。

算法实现:

public class BoyerMoore {private final int R; private int[] right;   private String pat;     public BoyerMoore(String pat) {this.R = 256;this.pat = pat;right = new int[R];for (int c = 0; c < R; c++)right[c] = -1;for (int j = 0; j < pat.length(); j++)right[pat.charAt(j)] = j;}public int search(String txt) {int m = pat.length();int n = txt.length();int skip;for (int i = 0; i <= n - m; i += skip) {skip = 0;for (int j = m-1; j >= 0; j--) {if (pat.charAt(j) != txt.charAt(i+j)) {skip = Math.max(1, j - right[txt.charAt(i+j)]);break;}}if (skip == 0) return i;}return n;              }
}

4  Rabin-Karp算法

Rabin-Karp算法是一种基于散列的子字符串查找算法--先计算模式字符串的散列值,然后用相同的散列函数计算文本中所有可能的M个字符的子字符串的山裂纸并与模式字符串的散列值比较。如果两者相同,再继续验证两者是否匹配。

算法思想:

长度为M的对应着一个R进制的M位数,

举例说明Rabin-Karp算法:例如要在文本3141592653589793中找到模式26535,首先选择散列表大小Q(这里设置为997),采用除留余数法,散列值为26535%997 = 613,然后计算文本中所有长度为5的字符串的散列值并寻找匹配。

算法实现:

实现Rabin-Karp算法关键是要找到一种方法能够快速地计算出文本中所有长度等于要匹配字符串长度的子字符串的散列值。也就是对所有位置i,  高效计算出文本中i+1位置的子字符串的值。具体算法为:假设已知h(xi) = xi mod Q, 将模式字符串右移一位等价于将xi替换为x(i+1), x(i+1)等于xi减去第一个数字的值,乘以R,再加上最后一个数字的值。这么做的结果是无论M是5、100还是1000,都可以在常数时间内不断地一格一格向后移动。

计算散列函数:

对于5位的数,可以用int直接计算,但如果M等于100、1000就不行了。这时候可以使用Horner方法。其计算方法如下:

private long hash(String key, int m) {   long h = 0; for (int j = 0; j < m; j++) h = (R * h + key.charAt(j)) % q;return h;
}

查找实现:

有两种代表实现:蒙特卡洛方法和拉斯维加斯方法。

  • 蒙特卡洛方法是选取很大的Q值,使得散列冲突极小,这样可以保证散列值相同就是匹配成功;
  • 拉斯维加斯方法则是散列值相同后再去比较字符,效率不如上一种方法,但可以保证正确性。
public class RabinKarp {private String pat;    private long patHash;   private int m;       private long q;       private int R;         private long RM;      public RabinKarp(String pat) {this.pat = pat;R = 256;m = pat.length();q = longRandomPrime();RM = 1;for (int i = 1; i <= m-1; i++)RM = (R * RM) % q;patHash = hash(pat, m);} private long hash(String key, int m) { long h = 0; for (int j = 0; j < m; j++) h = (R * h + key.charAt(j)) % q;return h;}private boolean check(String txt, int i) {for (int j = 0; j < m; j++) if (pat.charAt(j) != txt.charAt(i + j)) return false; return true;}public int search(String txt) {int n = txt.length(); if (n < m) return n;long txtHash = hash(txt, m); if ((patHash == txtHash) && check(txt, 0))return 0;for (int i = m; i < n; i++) {txtHash = (txtHash + q - RM*txt.charAt(i-m) % q) % q; txtHash = (txtHash*R + txt.charAt(i)) % q; int offset = i - m + 1;if ((patHash == txtHash) && check(txt, offset))return offset;}return n;}private static long longRandomPrime() {BigInteger prime = BigInteger.probablePrime(31, new Random());return prime.longValue();}
}