KMP算法个人理解

KMP算法的个人理解与实现

1. 算法介绍

KMP算法是一种模式匹配算法,什么是模式匹配?简单的理解,就是给定一个主串,以及一个模式串,在主串中匹配查找是否存在模式串并返回具体的位置,举个简单的栗子:

在这里插入图片描述

显然,上述模式串在主串中是有匹配的,那么如何通过计算机来实现模式匹配呢?看下面的代码:

int searchStr(char* s, int n, char* t, int m)		// s为主串,t为模式串
{
	for (int i = 0; i < n; i++)
	{
		int k = i;
		int j = 0;
		for (; j < m; j++)
		{
			if (t[j] == s[k])
			{
				k++;
			}
			else
			{
				break;
			}
		}
		if (j == m)	//模式串匹配完成
		{
			return i;
		}
	}
	return -1;
}

执行:

int main()
{
	char s[20];
	strcpy_s(s, "adsdcddsgadscsdfge");
	char t[5];
	strcpy_s(t, "adsc");
	
	int a = searchStr(s, 20, t, 4);
	cout << a << endl;

	system("pause");
}

​这是最暴力的对字符串进行匹配,基本原理就是从第一位开始,用模式串与主串挨个进行比较,如果遇到不匹配的部分,将整个模式串向前移动一个位置,重新比较,直到找到匹配的位置或者主串已经搜索完毕。

​很明显,上面的方法在对较小的数据可以使用,当数据量特别大时,执行效率就会很差,而KMP算法就是一种提高了效率的模式匹配算法。

2. 个人理解

  • next数组

    我们通过例子一步一步的来理解KMP算法是如何提高匹配效率的,看这样一个例子:

在这里插入图片描述

​按照暴力破解的方式,当模式串移动三次后匹配,会出现下面的情况:

在这里插入图片描述

​此时,c和f不匹配,我们需要将模式串后移一位,等等!先别急着后移一位,我们观察一下f的前面部分为cabca,是不是发现了什么,这一部分的开头两位和结尾两位都是ca,是不是可以考虑这样移动,即将模式串向后移动三位:

在这里插入图片描述

​没错!上一步中,我们已经比较过ca了,刚好模式串开头也是ca,显示是不用比较的,可以直接从开头ca后的一位开始比较,这样,就可以有效减少比较次数,提高效率。

那么问题来了,以上是我们通过观察的方法确定移动位置,那么在编码中,我怎么能知道当发现比较不同的位置后,应该从哪一位开始比较呢?

​现在,我们已经将问题转化为**当我在模式串的某一个位置跟主串比较时,发现比较失败,我应该重新从模式串的哪一个开始比较?**到这里我们就可以引入KMP算法的next数组了,next数组每个位置的值,就是对应模式串如果在这个位置匹配失败,应该重新从哪个位置匹配的值

​看下面的模式串:

在这里插入图片描述
当我们再第五位d匹配失败时,我们应该从第二位b开始重新匹配,因为第五位前面的a和第二位前面的a是一样的,没有必要再匹配,那么对应的next[4]=1;

当我们再最后一位匹配失败时,我们应该从第四位a开始重新匹配,因为第四位前面三位与最后一位的前面三位是一样的,对应next[8]=3。(数组下标从0开始计算)

上面两个例子是我们找某一个位置next值的过程,是用观察法来确定next值,但是用代码的方式如何来实现呢,我们一步一步来实现,先定义求next的方法,需要输入模式串,输出next数组,即:

void getNext(char* str, int n, int* next)
{
}

为了计算每个位置的next值,必然要对每一个位置进行一次遍历:

void getNext(char* str, int n, int* next)
{
  int i= 0;
  while(i < n)
  {
  }
}

对于第一个next值,这里特别规定为-1(后面解释原因),就有如下代码:

void getNext(char* str, int n, int* next)
{
  int i= 0;
  next[0] = -1;
  while(i < n)
  {
  }
}

其他位置的值确定,我们还需要一个辅助的索引,这个索引应该指向的位置是 该位置前面所有的部分构成的子串与当前正在计算next值的位置前面相同长度部分一致。emmm,听起来究极拗口,看下面的图解:

在这里插入图片描述

当i指向第六个位置a时,此时辅助索引j应该为i前面部分"ab"与模式串开头"ab"相对应的c的位置,即为j=2,按照上面的步骤,我们先初始化辅助索引,默认为-1(同next[0]后面解释原因):

void getNext(char* str, int n, int* next)
{
  next[0] = -1;
  int j = -1;
  int i= 0;
  while(i < n)
  {
  }
}

现在我们要考虑循环内部怎么实现了,思考一下str[i]和str[j]的值如果相同,代表什么含义?按照i和j的定义,此时j前面的两位应该与i前面的两位相同,那么如果str[i]==str[j],**说明i下一位的next值就是j+1!**即next[++i]=++j:

void getNext(char* str, int n, int* next)
{
  int i = 0;
  next[0] = -1;
  int j = -1;
  while (i < n)
  {
  	if (str[i] == str[j])
  	{
  		next[++i] = ++j;
  	}
  }
}

既然有相同的判定,必然也有不同的判定,如果不相同呢,next值是重新置为0么?我们再看个例子:

在这里插入图片描述

上面例子中,str[i]!=str[j],他们前面相同的部分为abcab,这是我们发现,单看这小块字符串,是不是也有公共部分,因此,当str[i]!=str[j],我们应该让j=next[j],即将j移动至第三位重复上面一步的比较:

在这里插入图片描述
此时代码可以如下改动:

void getNext(char* str, int n, int* next)
{
  int i = 0;
	next[0] = -1;
  int j = -1;
	while (i < n)
  {
		if (str[i] == str[j])
  	{
			next[++i] = ++j;
  	}
        else
      {
            j = next[j];
      }
	}
}

现在仔细观察我们的代码,发现当i=1时,计算的next[2],当i=2时,计算的是next[3],而next[0]的值我们已经规定为-1,循环只需要到n-2即可停止,同时,我们发现第一次循环时j==-1,需要加一个判定条件,于是:

void getNext(char* str, int n, int* next)
{
	int i = 0;
	next[0] = -1;
	int j = -1;
	while (i < n-1)			// 这里只需要循环到n-2
	{
		if (j == -1 || str[i] == str[j])	// 加入对j==-1的判定
		{
			next[++i] = ++j;
		}
        else
        {
            j = next[j];
      }
	}
}

想象一下next[0]为什么不置为0呢?如果next[0]置为0,当j == 0,str[0] != str[i],执行j=next[j]时,j会变成0,此时会重复执行这个过程陷入死循环,因此我们将next[0]以及j的初始值都设为-1以防止这种情况发生。

​ 以上就是整个next数组的完整求解方法!

  • KMP算法

到了这一步,我们已经得到了next数组,那么接下来就是利用next数组的比较过程了,同样的,逐步进行分析。首先,匹配方法需要输入两个字符串,并返回最终比较的结果,没有找到返回-1:

int KMP(char* s, int n, char* t, int m)
{
	return -1;
}

​ 首先需要计算next数组,并设置两个索引分别遍历主串和模式串,用i遍历主串,用j遍历模式串:

int KMP(char* s, int n, char* t, int m)
{
	int* next = (int*)malloc(sizeof(int)*m);
	int i = 0, j = 0;
	getNext(t, m, next);
	while (i < n && j < m)
	{

	}

	free(next);
	return -1;
}

​ 当i和j位置的值相同时,自然统一后移一位,当它们的值不同时,将j值置为对应的next值重新比较即可:

int KMP(char* s, int n, char* t, int m)
{
	int* next = (int*)malloc(sizeof(int)*m);
	int i = 0, j = 0;
	getNext(t, m, next);

	while (i < n && j < m)
	{
        // 比较i和j值
		if (s[i] == t[j])
		{
			++i;
			++j;
		}
		else
		{
			j = next[j];
		}
	}

	free(next);
	return -1;
}

​ 最后加上输出和结束判定语句:

int KMP(char* s, int n, char* t, int m)
{
	int* next = (int*)malloc(sizeof(int)*m);
	int i = 0, j = 0;
	getNext(t, m, next);

	while (i < n && j < m)
	{
		if (s[i] == t[j])
		{
			++i;
			++j;
		}
		else
		{
			j = next[j];
		}
	}
	free(next);
	
    // 结束判定
	if (j >= m)
		return i - m;    
	else
		return -1;  
}
  • 改进KMP算法

上述的KMP算法在一些特殊的情况下,可能效率会同样减少,需要对其进行改进,先看一个模式串匹配的例子:

在这里插入图片描述

​ 对上述模式串进行next数组求解,我们发现利用该next数组进行比较时,同样会有不必要的比较:
在这里插入图片描述

​ 为什么会出现这种情况呢?通过观察,我们发现,next[6] = 3,因此会继续而str[3](蓝色标记)比较,但是str[3]和str[6]的值都是a,str[6]刚才已经经过比较发现不匹配了,很明显用str[3]比较必然也不匹配(因为对应主串的i没有变,都是跟同一个str[i]比较),因此,需要对已有的next数组进行优化,得到新的优化只有的nextval数组,就是改进后的KMP算法。

​ 代码如下,只需要在计算next值的时候再判断一下str[next[j]]与str[j]是否一致:

void getNextVal(char* str, int n, int* nextval, int* next)
{
	int i = 0;
	next[0] = -1;
	next[0] = -1;
	int j = -1;
	while (i < n - 1)			// 这里只需要循环到n-2
	{
		if (j == -1 || str[i] == str[j])	// 加入对j==-1的判定
		{
			next[++i] = ++j;
			if (str[j] != str[next[j]])		// 不相等,next值与nextval值一致
			{
				nextval[i] = next[i];
			}
			else
			{
				nextval[i] = nextval[next[i]];	// 相等时nextval值为next[i]对应的nextval值,此处使用nextval[next[i]]是因为前面的nextval已经计算过了,是优化过的next值,如果用next[next[i]]则仍然可能有不是最优的可能
			}
		}
		else
		{
			j = nextval[j];		// 因为前面的nextval已经计算了,所以使用nextval进行回溯
		}
	}
}

3. 完整代码

  • next数组计算

    void getNext(char* str, int n, int* next)
    {
    	int i = 0;
    	next[0] = -1;
    	int j = -1;
    	while (i < n-1)			// 这里只需要循环到n-2
    	{
    		if (j == -1 || str[i] == str[j])	// 加入对j==-1的判定
    		{
    			next[++i] = ++j;
    		}
            else
            {
                j = next[j];
            }
    	}
    }
    
  • nextVal数组计算

    void getNextVal(char* str, int n, int* nextval, int* next)
    {
    	int i = 0;
    	next[0] = -1;
    	next[0] = -1;
    	int j = -1;
    	while (i < n - 1)			// 这里只需要循环到n-2
    	{
    		if (j == -1 || str[i] == str[j])	// 加入对j==-1的判定
    		{
    			next[++i] = ++j;
    			if (str[j] != str[next[j]])		// 不相等,next值与nextval值一致
    			{
    				nextval[i] = next[i];
    			}
    			else
    			{
    				nextval[i] = nextval[next[i]];	// 相等时nextval值为next[i]对应的nextval值
    			}
    		}
    		else
    		{
    			j = nextval[j];		// 因为前面的nextval已经计算了,所以使用nextval进行回溯
    		}
    	}
    }
    
  • KMP算法

    int KMP(char* s, int n, char* t, int m)
    {
    	int* next = (int*)malloc(sizeof(int)*m);
    	int* nextval = (int*)malloc(sizeof(int)*m);
    	int i = 0, j = 0;
    	getNextVal(t, m, nextval, next);
    
    	while (i < n && j < m)
    	{
    		if (s[i] == t[j])
    		{
    			++i;
    			++j;
    		}
    		else
    		{
    			j = nextval[j];
    		}
    	}
    
    	free(next);
    	free(nextval);
    
    	if (j >= m)
    		return i - m;    
    	else
    		return -1;  
    }
    

ps:如有错误,请多加指正,转载请注明出处!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值