字符串匹配详解

BF算法与KMP算法

  • 简介
  • BF算法
    • 思路
    • 示例
  • KMP
    • 思路
    • next数组
    • 示例

简介

      KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
      该算法是一种字符串匹配算法,功能是给一个主串(text)和一个模式串(pattern),计算主串中是否包含模式串,如果包含则返回模式串第一次出现在主串时的下标,否则返回-1。
      假设有两个串:

text = "abcbcglx";
pat = "bcgl";

      在这两个串中,我们可以明显看出text是包含pat的,并且pat出现在text的第四个字符上,对应的下标也就是3,即应该返回一个int值3。介绍KMP之前,我们先看看BF算法是如何进行匹配的。

BF算法

BF(Brute force),暴力算法。

思路

      (1)在BF中,要看text和pat是否存在包含关系,就要对两个串的字符进行逐一比对。我们约定使用j来代表text中当前要对比的下标位置,使用i来代表pat中当前要对比的下标位置。那么在上述的用例中,我们就要先将text中的第一个字符‘a’与pat中的第一个字符‘b’进行比对(即j=0,i=0)。如下图:
字符串匹配详解_第1张图片
      (2)这时发现text[j]pat[i]并不一致,因此下一步就需要从text的第二个字符开始跟pat比对(即j++)。如下图:
字符串匹配详解_第2张图片
      (3)现在,text[j]pat[i]一致,所以可以继续向下比较了,即比对它们各自的下一个元素(i++)。如下图。
字符串匹配详解_第3张图片
      (4)到了j=2,i=1时,text[j]pat[i]还是相等的,因此将j和i继续向后推(i++)。如下图。
字符串匹配详解_第4张图片
      (5)到了这步,发现此时text[j]pat[i]又不一致了,那么很遗憾,这时候要从text的第三个字符开始,重新跟pat的第一个字符开始比对了(即便pat[0]和pat[1]之前已经参加过比较)。
字符串匹配详解_第5张图片
      (6)这时候text[j]pat[i]还是不一致的,因此再将text的第四个字符与pat从头比。
字符串匹配详解_第6张图片
      此时text[j]pat[i]一致,继续向后推进,发现接下来推进的四个位置都是一致的,这样就已经从text中找到了pat,返回位置j。如果m代表text的长度,n代表pat的长度,BF算法的时间复杂度为O(mn)

示例

int BruteForse(char text[], char pat[])
{
    for (int j = 0; j <= strlen(text)-strlen(pat); j++){
    	int i;
        for (i = 0; i < strlen(pat); i++){
            if (text[j+i] != pat[i])){
                break;
            }
        }
        if (i == strlen(pat)){
            return j;
        }
    }
    return -1;
}

KMP

      在BF中,pat串每次匹配失败,都要回到第一个位置重新比较,那么之前参加过比较的也都要再次进行比较。KMP算法就是为了尽可能的减少这种重复性工作,让pat串已经比较过的一些字符可以不用再次比较。

思路

      假设有两个串:

text = "abcxabcdabxabcdabcy";
pat = "abcdabcy";

      (1)令text开始比较的元素下标是jpati。开始时j=0,i=0
字符串匹配详解_第7张图片
      (2)此时发现text[j]==pat[i],就继续比后续的字符(i++,j++),直到text[3]和pat[3]:
更正:下图中j应该在3上方
字符串匹配详解_第8张图片
      (3)此时对比的两个字符不相等,对于pat串,观察i之前已经对比过的序列abc,观察其中有没有相同的前缀和后缀,发现并没有,因此将i回退至0
字符串匹配详解_第9张图片
(4)这时,两个字符同样不等,因此将j后推进即可(j++)。
字符串匹配详解_第10张图片
      (5)到了这步发现相等,继续向后比对,发现直到比到text[10]与pat[6]时,出现不等情况。
更正:下图中j应该在10上方
字符串匹配详解_第11张图片
      (6)此时发现,在i之前的串abcdab中,存在着相同的前缀和后缀,其中最长的是ab,因此在pat中直接令i=2,即ab后面的字符。同时更新j=10
字符串匹配详解_第12张图片
      (7)此时出现不等,那么观察i以前的序列ab,发现没有相同的前缀或后缀,那么就让重置i=0,j向后推动(j++)。
字符串匹配详解_第13张图片
      (8)随后一直进行,即可发现后面的元素都是相等的,那么可以说pat已经在text中被找到,最终匹配完j的值应该是19,用19减去pat的长度,得11,即为pat出现的第一个位置。
      总结上述过程,我们可以发现:

  • 当对比字符匹配时,需要将i++,j++
  • 当发生不匹配时,有两种情况:一是pat的第一个字符就不匹配,这时候最好处理,pat下次还需要从头比,所以i不用动,j直接后移一位j++即可。
  • 不匹配时的另外一种情况是:pat串前面已经有一段发生匹配了,也就是在这时候才能体现出KMP的优点。这时候我们先不用管j,然后找到i之前的串中相同的前缀和后缀,并且取长度最长的一个(假设为l),则令i=l
          总结好了上述规律,就可以开始编写函数了,不过这时候还有一个问题,就是该如何确定一个串中相同的前缀与后缀中最长的长度,也就是上面提到的l。我们通过定义一个数组,来记录串中每个元素前面的串的l值,也就是所谓的next[]数组。

next数组

      (1)假设要找“abcaby”的next数组,首先将next[0]赋值为0。随后设两个标记变量j=0,i=1
字符串匹配详解_第14张图片
      (2)接下来将pat[j]pat[i]作比较,由于不等,且j=0,就将next[i]赋值为0,然后i++,同理next[2]也是0
字符串匹配详解_第15张图片
      (3)随后i走到了a的位置,这时候pat[j]==pat[i]成立,就令next[i]=j+1,此时next[3]=1,然后j++,i++。同理可得next[4]=2
字符串匹配详解_第16张图片
      (4)这时候又出现了pat[j]!=pat[i],但与第二步不同,此时的j是不等于0的,因此还需要特殊处理一下:令j=next[j-1],此时j又回到了0,再进行比较,便又回到了pat[j]!=pat[i] && j==0的情况,因此next[5]也等于0。到这里,next数组就已经初始化完毕。

示例

      由上述过程可先编写出初始化next数组的函数:

void initNext(char pat[]){
	int j = 0, i = 1;
	int size = strlen(pat);
	while(i < size){
		if(pat[i] != pat[j]){
			if(j==0){
				next[i] = 0;
				i++;
			} else {
				j = next[j-1];
			}	
		} else {
			next[i] = j+1;
			i++;
			j++;
		}
	}
}

      根据上述过程,KMP算法的代码为:

#include
#include  
using namespace std;
char text[50] = "abxabcabcaby";
char pat[20] = "abcaby";
int next[20] = {0};
void initNext(char pat[]){
	int j = 0, i = 1;
	int size = strlen(pat);
	while(i < size){
		if(pat[i] != pat[j]){
			if(j==0){
				next[i] = 0;
				i++;
			} else {
				j = next[j-1];
			}	
		} else {
			next[i] = j+1;
			i++;
			j++;
		}
	}
}

int KMP(char text[], char pat[]){
	int j = 0;//text串中的标记 
	int i = 0;//pat串中的标记 
	int length = strlen(text);
	while(j < strlen(text)){
		if(text[j] != pat[i]){
			if(i == 0){
				j++;
			} else {
				i = next[i-1];
			}
		} else {
			i++;
			j++;
		}
	}
	if(i == strlen(pat)){
		return j - strlen(pat);
	}
	return -1;
}

int main(){
	initNext(pat);
	cout<<"pat串的next数组为:"; 
	for(int i=0;i<strlen(pat);i++)
		cout<<" "<<next[i];
	cout<<endl;
	int ans = KMP(text, pat);
	if(ans != -1) 
		cout<<"pat在text中第一次出现的位置是:"<<ans;
	else
		cout<<"text不含pat"; 
	return 0;
} 

      在KMP中,时间复杂度为O(m),但构建next数组的时间复杂度为O(n),因此总的时间复杂度为O(m+n),远比BF好很多。

你可能感兴趣的:(算法)