• 算法之路-------KMP算法


    KMP算法由来

    KMP算法是一种改进的字符串匹配算法,由 D.E.KnuthJ.H.MorrisV.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次
    数以达到快速匹配的目的
    。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n) [1] 。 来自-------百度百科。

    KMP算法作用

    主要作用是用来解决字符串匹配问题,他与我们一般情况下所想的暴力解法不同,对于一个字符串匹配问题,我们如果用暴力解法例如如下数据:
    在这里插入图片描述

    如上图:如果我们使用暴力解法的话,对主字符串进行匹配,当我们匹配到一个位置,这个位置的主字符串和要匹配的字符串不相同时,这时,要匹配的字符串就又需要从第一个位置往后进行遍历,只要出现不匹配的一个字符,又要让匹配的字符串从第一个位置往后遍历,并且主字符串上的指针也要回退一格,然后往后匹配。就像上图中的往后遍历 i 所对应的主字符串的位置上的字符与 j 所对应的匹配字符串上的字符不一样时,j 就要重新返回匹配字符串的首位置,重新开始遍历,并且i也回退一格
    暴力算法也叫BF算法,而上图,的实现循环的算法如下:

    while(i  < strLen && j < subLen)
    {
       if(str[i] == sub[j])
       {
          i++;
          j++;
       }
       else
       {
          i = i-j+1;//回退,会发现,此时i只回退一格,如果还不匹配,那么i就会一直往前走了
          j = 0;
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    但是,对于KMP算法,他想到了一个办法,就是当我们匹配字符串在主字符串中匹配时,如果出现了不匹配,我们可以让指向匹配字符串的遍历指针不是每次都指回第一个数重新来,我们可以让其指向该匹配字符串的一个特殊位置,而这个特殊位置是匹配字符串对于在主字符串中已经匹配的数据相对于匹配字符串可以调整的大位置,所以对于KMP算法,相对于BF算法有两个可更新的地方:

    ①:主串上的指针不会退
    在这里插入图片描述
    ②:匹配字符串的指针不需要每次退回到第一个指针位置

    在这里插入图片描述
    而对于这个特定的计算方法,可以让我们每次都能将 j 指针指向最大不可重复的位置,并且,我们将需要匹配的字符串的每个字符,都会通过这个算法计算出来,放在一个数组中,这样当我们在真正使用的时候,直接用数组对应的下标使用就可以了,而这个数组就是下面的next数组。

    next数组

    通过上面的描述,我们知道,next数组的本质就是存放需要匹配的字符串中,每个字符如果匹配不上,那么这个当前这个字符串的指针应该指向哪个下标是最有效率的位置,我们用 j 表示当前所指向的字符串的下标位置,用 k 表示当前j所指字符的如果在主字符串上匹配不上,然后 j 要重新指的位置,那么:j = next[k]。此时直接一行代码搞定,但是对于不同的 j 来说,他会对应不同的,并且此时这个j 和 k其实是大小相等的,只不过 j 所指的是要匹配的字符串的位置,而 k 所指的是next数组中当前 k 位置出错时,j应该指向的位置。我们了解了next数组的用法,最终要的其实就是next数组的计算方法了,如下:

    next数组计算方法

    1.规则:

    • 找到匹配成功的两个相等的字符串,一个是以下标为0开始,一个是以j = i-1下标结束。
    • 不管是什么数据,他们所对应的next数组的0下标为-1,1下标为0,即:next[0] = -1,next[1] = 0。

    2.我们通过上面的规则,我们来推导以下以下字符串:
    在这里插入图片描述

    3.通过上述,我们就可以知道如何去计算了,但是,如何通过电脑运行去解决这个问题呢,并且我们目前只知道next[i] = k,但是next[i+1] = ?,这个问题要让我们去解决。如果我们通过next[i]的值可以推出next[i+1]的值,那么这个问题的解决就有点好解决了。
    首先我们先看如下数据:

    在这里插入图片描述
    我们会发现,如果对应一个数下标的next数组对应的值是与它前一个数的next数组对应的值是有关的,其中,如果是增长的话,那么只会比前一个数的对应的next数组的值大1,如:
    在这里插入图片描述
    这是为什么呢?因为对于一个这样的一个数组,因为如果匹配的时候如果在当前位置出错了,前面的数对应下标的next数组的值如果以其作为sub的下标时,两个字符是相等的,那么就证明,我们是可以通过前一个数的下标来判断后一个数的next数组对应下标的数,那么如何做呢,如下:

    • 首先,我们假设next[i] = k成立(其中i为对应的sub字符串下标,k为改下标所对应的next数组值),有了这个式子,那么我们就可以退出来,在sub字符串中有sub[0]到sub[k-1]组成的字符串和sub[x]到sub[i-1]组成的字符串是相等的,这也就是我们在规则中出现的有两个字符串相等的情况,如下图:
      在这里插入图片描述
      如上图所示例子,我们所对应的x就可以找到了,那么我们就可以列出下面这个式子:sub[0]到sub[k-1] = sub[x]到sub[i-1],又因为对应的字符串相等,则有式子:k-1-0 = i-1-x,那么就可以得出x = i-k,所以对应的上述式子变成了sub[0]到sub[k-1] = sub[i-k]到sub[i-1],如果此时又sub[i] = sub[k],那么sub[0]到sub[k] = sub[i-k]到sub[i],那么对应的下标next[i+1] = k+1。(因为此时,对应的i增加,那么对应的k也必须增加,所以对于next[i+1],它的值会增加为1),当然,这是当sub[i] = sub[k]的情况下的。
    • sub[i] != sub[k]:如果此时出现这种情况,那么我们就要看下图:
      在这里插入图片描述
      如上图,此时i下标对应的k值为2,而此时sub[i] !=sub[k],此时对于这种情况,就要让k继续往前跟进,那么此时k=next[k],则k=0,但是这次回退后,对应的sub[i]==sub[k]了,那么此时给k+1,就对应的是next[i+1]的值了。(原理和第一步的一样,因为一直回退,只要出现相同的了,那么证明相同哪个位置对应next[x]的大小,就是哪个位置前面的哪个重复字符串的大小长度,我们就可以少遍历几次)

    next数组优化

    对于next数组其实是还可以优化的,如遇到以下情况:
    在这里插入图片描述

    一旦出现在a的范围内出现不匹配,那么就会一直往前倒退,直到倒退到0,对于这种情况,如果我们一步到位,那岂不是很好,所以我们有了这样的一个做法,就是如果说在相同的字符下出错了,我们就可以直接赋值对应哪个字符的下标即可,所以,优化后的上述next数组如下:

    在这里插入图片描述

    KMP算法模拟实现

    #include<iostream>
    #include<string>
    #include<vector>
    using namespace std;
    vector<int> GetNext(string& sub)
    {
    	vector<int> next(sub.size());
    	next[0] = -1;
    	next[1] = 0;
    	int i = 2;  //从第二个位置开始
    	int k = 0;  //指向的是前一项的k值
    	while (i < sub.size())
    	{
    		if (k == -1 || sub[i-1] == sub[k])  //万一前面没有匹配的,那么k有可能会回到next数组第一个位置,就变成了-1了。
    		{
    			next[i] = k + 1;
    			i++;
    			k++;
    		}
    		else
    		{
    			k = next[k];
    		}
    	}
    	return next;
    }
    int Kmp(string& str, string& sub, int pos)
    {
    	if (str.size() == 0 || sub.size() == 0 || sub.size() > str.size())
    	{
    		return -1;
    	}
    	if (pos < 0 || pos >= str.size())
    	{
    		return -1;
    	}
    
    	vector<int> next = GetNext(sub);
    	int i = pos;//从pos位置开始遍历str
    	int j = 0;  //从0位置开始遍历sub
    
    	while (i < str.size() && j < sub.size())
    	{
    		if (j == -1 || str[i] == sub[j])
    		{
    			++i;
    			++j;
    		}
    		else
    		{
    			j = next[j];
    		}
    	}
    	if (j == sub.size())
    	{
    		return i - j;
    	}
    	return -1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
  • 相关阅读:
    单词方阵-(棋盘问题)
    2022PMP项目管理认证考试报考指南(2)
    【打卡】牛客网:BM54 三数之和
    Linux——匿名管道、命名管道及进程池概念和实现原理
    MySQL之事务隔离级别和MVCC
    Ps:RGB 直方图
    【人脸生成】HiSD-通过层级风格解耦实现图到图的迁移
    高压放大器选型标准规范要求有哪些
    [C++ 网络协议] 多线程服务器端
    Taurus.mvc .Net Core 微服务开源框架发布V3.1.7:让分布式应用更高效。
  • 原文地址:https://blog.csdn.net/xiaobai_hellow/article/details/124917462