最近看《大话数据结构》,在串这种数据结构里面提到了kmp算法,在网上搜了一下又发现了更好的Boyer-Moore算法。
这里整理一下两种算法。(此篇文章主要用于记忆)
在字符串的匹配过程中大家很容易想到从首字符一个一个的去比较,
最快的情况:test(abcdefg),pattern(abc),这里匹配了三次就找到了,时间复杂度O{1}.
最慢:test(0000001),pattern(001),每一次前面两个0都要比较一下,比较了(7-3+1)*3=15次。时间复杂度为O{(n-m+1)*m}.
显然这样的暴力匹配是很费时的。
下面来介绍一下这两种算法得思想
kmp:e于c不等,在已经匹配的字符中找到相同的前缀(ab)和后缀(ab),这样直接用前缀的后一个字符和模板中的下一个字符比较(也就是为了不使模板中的字符发生回溯)。
这里移动的距离是:已经匹配的长度(6)-相同的前后缀长度(2)=4
Boyer-Moore:这种算法直接换了一种思考模式,不再从前往后一个一个匹配,直接匹配pattern的最后一个字符,也就是从后往前匹配。不匹配则直接跳到下一个位置,匹配则往前移动到不匹配的字符,并用坏字符和好后缀算法得出移动位置。(详情见参考文章)
这里直接给出代码:
/**
* 用于生成部分匹配表
* 例:ABCDABD-->[0,0,0,0,1,2,0]
* @param sub
* @return
*/
private static int[] kmpnext(String sub) {
int[] n = new int[sub.length()];
n[0]=0;
int x = 0;
for (int i = 1; i < sub.length(); i++) {
while (x > 0 && sub.charAt(i) != sub.charAt(x)) {
x = n[x - 1];
}
if (sub.charAt(i) == sub.charAt(x)) {
x++;
}
n[i] = x;
}
return n;
}
/**
* kmp匹配法
* @param str
* @param sub
* @return
*/
public static int kmp(String str, String sub) {
if(str == null || sub == null || str.length() == 0 || sub.length() == 0){
throw new IllegalArgumentException("str或者sub不能为空");
}
List returnList=new ArrayList();
int j = 0;
int[] n = kmpnext(sub);
for (int i = 0; i < str.length(); i++) {
while(j > 0 && str.charAt(i) != sub.charAt(j)){
//j=j-(j-n[j-1])=n[j-1](移动位数 = 已匹配的字符数 - 对应的部分匹配值)
/*
* 匹配字符:abddcab,目标字符:abddcacabddcab。当b与c不等时,不需要再用a与bddca比较,直接可得出a=a,
* 再接着比较两个a后面的字符
* */
j = n[j - 1];
}
if(str.charAt(i) == sub.charAt(j)){
j++;
}
if(sub.length() == j){
int index = i - j + 2;
//j=0;
returnList.add(index);
return index;
}
}
return -1;
}
/**
* 好后缀:如果程序匹配了一个好后缀, 并且在匹配字符串中还有另外一个相同的后缀或后缀的部分, 那把下一个后缀或部分移动到当前后缀位置。
* @author sunzhe
* @param len,pattern,
* */
public static int[] moveByGood(String pattern,int len){
int i,j;
int[] suff = new int[len];
int[] returnArray = new int[len];
// 计算后缀数组
suff = suffix(pattern,len);
//Case3:如果完全不存在和好后缀匹配的子串,则右移整个模式串。
for(i = 0;i < len; i++){
returnArray[i] = len;
}
//Case2:如果不存在和好后缀完全匹配的子串,则在好后缀中找到具有如下特征的最长子串,使得P[m-s…m]=P[0…s]。
for(i = len-1; i >= 0; i--){
if(suff[i] == i + 1)
{
for(j = 0; j <= len - 1 - i - 1; j++)
{
if(returnArray[j] == len)
returnArray[j] = len - 1 - i;
}
}
}
//Case1:模式串中有子串和好后缀完全匹配,则将最靠右的那个子串移动到好后缀的位置继续进行匹配。
for(i = 0; i < len - 1; i++){
returnArray[len - 1 - suff[i]] = len - 1 - i;
}
return returnArray;
}
/**
* 后缀数组(匹配字符部分和全部的相同个数)
* @author
* @param len,pattern
* */
public static int[] suffix(String pattern,int len){
int[] returnArray = new int[len];
int f,i;
returnArray[len-1]=len;
for(i=len-2;i>=0;i--){
f=i;
while(f>=0 && pattern.charAt(f)==pattern.charAt(len-1-i+f))
f--;
returnArray[i]=i-f;
}
return returnArray;
}
/**
* 坏字符
* @author sunzhe
* @param c,pattern,j
* */
public static int moveByBad(int j,String pattern,char c){
boolean flag=false;
for(int i=0;i<pattern.length();i++){
if(pattern.charAt(i)==c){
flag=true;
}
}
if(!flag){
return j-(-1);
}else{
return j-last(pattern,c);
}
}
/**
* @author sunzhe
* 搜索词中的上一次出现位置
* */
public static int last(String pattern,char c){
for(int i=0;i<pattern.length();i++){
if(pattern.charAt(i)==c && pattern.charAt(i+1)!=c){
return i;
}
}
return -1;
}
/**
* @author sunzhe
* Boyer-Moore算法
* */
public static int bM(String pattern, String test){
int j=pattern.length()-1;
int i=0;
i=j;
int[] good=moveByGood(pattern, pattern.length());
while(i<test.length()){
if(test.charAt(i)==pattern.charAt(j)){
i--;
j--;
}else{
char c=test.charAt(i);
i=i+Math.max(moveByBad(j, pattern, c), good[j]);
j=pattern.length()-1;
}
if(j==0){
return i+1;
}
}
return -1;
}
注:
好后缀算法中先把整个数组赋值,在根据情况(if条件)修改数组中的值。这里很好体现了算法转为程序的思维。
参考文章:http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
http://blog.jobbole.com/52830/