KMP字符串匹配算法

来源:互联网 发布:大麻种子淘宝 编辑:程序博客网 时间:2024/06/03 18:52
原文地址:KMP字符串匹配算法作者:沙子泥巴刘

   昨天微创的第一题,我就用了最笨最傻的朴素匹配方法......一定被BS死了。我知道存在这么个KMP字符串匹配算法,但是具体内容却说不上来。今天查了半天,总算找到一个讲得比较清楚的了。转载网址:http://yxjoey.is-programmer.com/posts/1682.html

 

最笨的算法,无视此算法:

int strstr(char *sub, char* str){
  int i=0;
  char *p=str, *q=sub;
 while(*(p+i)!=''&&*(q+i)!=''){
  if(*(q+i)==*(p+i))
  i++;
  else{
  p++;
  i=0;
  }
  }  
  if(*(q+i)=='')
  return p-str;
  return -1;
}

 

KMP算法在子串匹配失效的时候,下一步并不是重新从子串的头部开始匹配,而是根据一下next函数计算出下一步应该从子串的什么部位开始匹配。举例如下:

[转载]KMP字符串匹配算法

红色为失效位置, '^'表示的当前是指针位置,'~'表示此次匹配A串开始的位置。

若是普通的匹配算法,当失效时,C串指针要回溯到头部,A串指针也要回溯到'~'的下一位。也就是下一步应该是从A的第二字符(e.g.'b')和C的开头(e.g. 'a')开始匹配。如此循环

[转载]KMP字符串匹配算法

直到找到A串中的某一个位置可以匹配C串。

然而从上面的匹配过程中,可以发现A和B的蓝色部分是第一步中已经确认匹配过的,上面四步的匹配其实可以看作是蓝色部分的前半段与后半段在进行比较,而且前三步在蓝色部分就已经匹配失效了,所以这些比较是无谓的,因为它与父串A无关的,是属于子串C本身的信息。只有第四步才涉及了与父串A中的字符('c')的比较

KMP算法正是利用这一点,它事先利用子串本身的信息就计算出当某一次匹配失效时,下一次应该继续匹配的位置。也就是当C串在最后一个'b'匹配失效时,它省略了前三步(1,2,3)无谓的匹配,下一步比较将在'd'与'c'之间进行。这样父串A无需回溯,C串用一个next函数决定它将回溯的位置。所以next函数至关重要,也是这个算法的关键。

从上面的分析可以知道next函数是子串C本身的性质决定的,假设子串P =P0P1...Pn-1

next(j)=k(k>=0): 当P的第 j+1个字符 匹配失败时,在父串指针不回溯的情况下,下一步将与Pk+1比较。

当next(j)=k (k>=0)时,子串指针回溯到Pk+1的位置,父串指针不变;
当next(j)=-1时,子串指针回溯到头,父串指针前进一步;

在设计计算next值的程序的时候,我们没有必要每一步都去计算maximum(k),可以用递归方式来做

举个例子
假设子串为P:"abacabab",且我们将要求的是'b'的next值, e.g. next[7]
假设next[0~6]均为已知: next[0]=-1, next[1]=-1 , next[2]=0 , next[3]=-1 ,next[4]=0 , next[5]=1 ,next[6]=2

  "abacabab"

next[6]=2可以说明P[0~2](蓝)与P[4~6](红)是一样的

要求next[7]的值,我们可以找前6位("abacaba")中最长的前半段与后半段相等的子串,然后比较前半段子串的下一位是否和P[7]相等。在这个例子中,P[0~next[6]](e.g. P[0~2])就是这样的子串,接下来我们比较 c 和b也就是P[next[6]+1]('c')和P[7]('b').
若相等则 next[7] = next[6]+1
若不等, 我们进一步找更短的前半段与后半段相等的子串,因为aba和aba是一样的,要找'abacaba'中比'abc'短的这样的子串,相当于找'aba'中的next[2]值是一样的.也就是next[next[6]]的值。然后再比较P[next[next[6]]+1]和P[7]是否等,若不等则继续找更短的这样子串


在上的例子中 P[next[6]+1]=P[3]('c') ,与P[7]('b')不相等, 但是 P[next[next[6]]+1]= P[next[2]+1] = P[1]('b'), 与P[7]('b')相等

最后可以得到next[7] = next[next[6]]+1 = next[2]+1= 1;

计算next值的代码:
void calnext(char* P, int next[]){
  next[0] = -1; //第一个元素的next总是-1, 因为根据(1) ,我们并找不到一个k比j=0小
  for(int i=1; i<strlen(P); i++){
  int k = next[i-1]; //因为采用递归的方法, 为了计算next[i], 先记录下next[i-1]而且假设next[i-1]已知
 while(P[k+1]!=P[i]&&k>=0){// 递归
  k = next[k];
  }
  if(P[k+1]==P[i]) //如果相等而结束, 则找到一对长度为k的前缀字串和后缀字串
  next[i] = k+1; //增加了一个相同项
  else
  next[i] = -1; //其他情况
  }
}

匹配的代码:
int find(char*T, char* pat){
  int n = strlen(pat);
  int *next = new int[n];
  calnet(pat, next);
  char *p=T, *q=pat;
  int i=0;
 while(*p!=''&&(*(q+i)!='')){
  if(*p==*(q+i)){
  p++;
  i++;
  }else{
  if(i==0)
  p++;
  else
  i = next[i-1]+1;
  }
  }
  if(*(q+i)=='')
  return p-T-n;
  else
  return -1;

 

记录一下另一套代码,道理是一样的,不过是next数组的起始值差1,转载地址:http://www.blogjava.net/renyangok/archive/2007/03/05/101927.html

失效函数的算法:

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;
}

如果要求支持通配符?和*,可改进上述kmp算法:

通配符?

失效函数:

...
 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;
...
匹配函数:

...
 while(target_index < token_length&& pattern_index <pattern_length) {
  if(target[target_index] == pattern[pattern_index]||pattern[pattern_index] == '?') {
  target_index++;
  pattern_index++;
  } else {
...


通配符*:

*的作用是匹配任意多个字符,显然我们不能简单的修改匹配过程而满足要求。如果我们重新思考*的作用,我们会发现*的另一个作用就是分割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;
}

0 0
原创粉丝点击