zoukankan      html  css  js  c++  java
  • KMP 算法简单解释

    讲KMP算法,离不开BF,实际上,KMP就是BF升级版,主要流程和BF一样

    不同是在匹配失败时能利用子串的特征减少回溯,利用根据子串特征生成的Next数组来减少

    <( ̄︶ ̄)↗[GO!]

    !!!所有数组下标都是从0开始

    1. 先看看BF算法(暴力破解)

    int Brute_force_1(const char *S, const char *T)
    {
    	if (!S || !T)
    		return -1;
    	int lenS = strlen(S);
    	int lenT = strlen(T);
    	int i = 0;				//主串下标索引
    	int j = 0;				//子串下标索引
    	while(i < lenS && j < lenT)
    	{
    		if (S[i] == T[j])	//如果相等一直继续往下匹配
                ++i,++j;
    		else				//不相等i和j开始回溯
    		{
    			i = i-j+1;
    			j = 0;
    		}
    	}
    	if (j == lenT)
    		return i - j;
    	return -1;
    }
    

    ​ BF算法有几种不同实现,但最终思想都是一样的,以下就是另一个BF实现

    int Brute_force_2(const char *S, const char *T)
    {
    	if (!S || !T)
    		return -1;
    	int lenS = strlen(S);
    	int lenT = strlen(T);
    	for (int i = 0; i <= lenS - lenT; ++i)
    	{
    		int k = i, j = 0;
    		while (k < lenS && j < lenT && S[k] == T[j])
    		{
    			++j;
    			++k;
    		}
    		if (j == lenT)
    			return i; //说明匹配到了
    	}
    	return -1;
    }
    

    ​ 你完全可以根据自己的理解写出BF算法,但在这里,为了BF和KMP统一,我们还是采用第一种实现,即容易看出回溯操作的实现

    2. Next[]数组

    ​ 事实上,书上的next数组生成算法是经过优化后的算法,比较难懂,但你完全可以按照自己的理解做一个

    注意:Next[]数组只是在KMP中字符串匹配失败时使用的

    void GetNext(int Next[], char *str)
    {
    	assert(str!=NULL);
    	int len = strlen(str);
    	if(len>1)Next[0]=0;
    //其实Next[0]等于0或者等于-1效果没什么影响,
    //因为在KMP中不匹配时判断是不是第一个字符不匹配用用的是j==0;-----if (j==0||Next[j]==0),
    	if(len>2)Next[1]=0;
    //Next[]等于0时说明需要讲i回溯到子串头的下一个位置(i=i-j+1);
    //此时j也回到子串头位置(j=0)
    	for(int i=2;i<len;++i)
    	{
    		for(int j=i-1;j>0;--j)
    		{
    			if(!strncmp(&str[0],&str[i-j],j))
    			{
    				Next[i]=j;break;		//找到最大重复子子串(子串中的子串)
    //Next[]为其他值则i不变,讲j回溯到Next[j]的位置(j=Next[j])
    			}
    			else Next[i]=0;
    		}
    	}
    }
    
    

    ​ 这个时间复杂度要比书上的方法高很多,但好理解,真实的反映了Next数组的本质。

    3. KMP

    int KMP(const char *S, const char *T, const int *Next)
    {
    	if (!S || !T||!Next)
    		return -1;
        int lenS = strlen(S);
    	int lenT = strlen(T);
    	int i = 0;				//主串下标索引
    	int j = 0;				//子串下标索引
    	while(i < lenS && j < lenT)
    	{
    		if (S[i] == T[j]) ++i,++j;	//若相等则继续匹配下一个字符
    		else		//不相等则回溯
    		{
                //(当j==0时,即第一个字符不匹配,和Next[j]==0时事实上与BF算法相同)
    			if (j==0||Next[j]==0)
                {
    				i = i-j+1;
    				j = 0;
    			}
    			else j = Next[j];//主串i位置不变,讲子串下标索引挪到Next[j]的位置
    		}
    	}
    	if (j == lenT)
    		return i - j;
    	return -1;
    }
    
    

    ​ 这个回溯时的操作实际上是把两种情况合成一种,拆开后就是下面的,就是生成next数组那块三种情况

    while (i < lenS && j < lenT)
    {
    	if (S[i] == T[j])
    		++i, ++j;
    	else
    	{
    		if (j == 0)
    		{
    			++i; //等价于i = i-0+1;j本身就等于0
    		}
    		else if (Next[j] == 0)
    		{
    			i = i - j + 1;
    			j = 0;
    		}
    		else
    		{
    			j = Next[j];
    		}
    	}
    }
    

    扩展

    ​ Next数组有进一步改进的可能,如果发生失配,失配点子串字符若与回溯到的字符相同,则再次匹配肯定失败,所以改进的Next数组进一步处理了这种情况,消除了回溯

    void GetNext_pro(int Next[], const char *str)
    {
    	assert(str!=NULL);
    	int len = strlen(str);
    	if(len>1)Next[0]=-1;
    //其实Next[0]等于0或者等于-1效果没什么影响,
    //因为在KMP中不匹配时判断是不是第一个字符不匹配用用的是j==0;-----if (j==0||Next[j]==0),
    	if(len>2)Next[1]=0;
    //Next[]等于0时说明需要讲i回溯到子串头的下一个位置(i=i-j+1);
    //此时j也回到子串头位置(j=0)
    	for(int i=2;i<len;++i)
    	{
    		for(int j=i-1;j>0;--j)
    		{
    			if(!strncmp(&str[0],&str[i-j],j))
    			{
    				if(str[i]==str[j])
    					Next[i]==Next[j];
    				else
    					Next[i]=j;
    				break;		//找到最大重复子子串(子串中的子串)
    //Next[]为其他值则i不变,讲j回溯到Next[j]的位置(j=Next[j])
    			}
    			else Next[i]=0;
    		}
    	}
    }
    

    测试代码

    int KMP(const char *S, const char *T)
    {
    	if (!S || !T)
    		return -1;
    	int Next[MAXSIZE] = {0};
    	GetNext(Next,T);
    	print_arr(Next, strlen(T));
    	GetNext_pro(Next,T);
    	print_arr(Next, strlen(T));
    
        int lenS = strlen(S);
    	int lenT = strlen(T);
    	int i = 0;				//主串下标索引
    	int j = 0;				//子串下标索引
    	while(i < lenS && j < lenT)
    	{
    		if (S[i] == T[j]) 
    			++i,++j;	//若相等则继续匹配下一个字符
    		else		//不相等则回溯
    		{
                //(当j==0时,即第一个字符不匹配,和Next[j]==0时事实上与BF算法相同)
    			if (j==0||Next[j]==0)
                {
    				i = i-j+1;
    				j = 0;
    			}
    			else j = Next[j];//主串i位置不变,将子串下标索引挪到Next[j]的位置
    		}
    	}
    	if (j == lenT)
    		return i - j;
    	return -1;
    }
    int main(void)
    {
    	char source[MAXSIZE] = "adcfabadcf";
    	char target[MAXSIZE] = "abcabcabbac";
    	printf("%d
    ", Brute_force_1(source, target));
    	printf("%d
    ", Brute_force_2(source, target));
    	printf("%d
    ", KMP(source, target));
    	
    	getchar();
    	return 0;
    }
    
    附上BF与KMP的比较,你会发现两者其实挺相似
    

    总结

    ​ 其实核心就在于本文第一句话的理解。

    ​ KMP在子串含有相同前后缀时,利用Next数组减少匹配失败时的回溯次数有优势,而改进的Next数组在此基础上若子串含有较多相同字符则更进一步减少回溯。

    ​ 所以KMP总之是利用子串的特征来削除回溯,如果子串并不具有这些特征,那就还没有BF好,因为KMP还需要额外的空间来存放Next数组

    ​ 书上的next数组的生成很难懂,加油理解中。。。(ง •_•)ง

  • 相关阅读:
    解决Windows 10每次重启默认浏览器都被重置为IE的一个办法
    使用cookie登录百度网盘账号
    【C++ Primer | 14】重载运算
    Visual Studio Code 快捷键的使用
    【C++ Primer | 8】IO库
    Git push常见用法
    Git 基础
    Git连接GitHub仓库详解
    ceph关于rpm包构建的教程
    monitor综合
  • 原文地址:https://www.cnblogs.com/starrys/p/11625840.html
Copyright © 2011-2022 走看看