关于KMP及其改进的思考

(1)首先推荐我见过的最好的KMP算法详解(转载自知乎:https://www.zhihu.com/question/21923021/answer/281346746):

有些算法,适合从它产生的动机,如何设计与解决问题这样正向地去介绍。但KMP算法真的不适合这样去学。最好的办法是先搞清楚它所用的数据结构是什么,再搞清楚怎么用,最后为什么的问题就会有恍然大悟的感觉。我试着从这个思路再介绍一下。大家只需要记住一点,PMT是什么东西。然后自己临时推这个算法也是能推出来的,完全不需要死记硬背。KMP算法的核心,是一个被称为部分匹配表(Partial Match Table)的数组。我觉得理解KMP的最大障碍就是很多人在看了很多关于KMP的文章之后,仍然搞不懂PMT中的值代表了什么意思。这里我们抛开所有的枝枝蔓蔓,先来解释一下这个数据到底是什么。对于字符串“abababca”,它的PMT如下表所示:
 

 

就像例子中所示的,如果待匹配的模式字符串有8个字符,那么PMT就会有8个值。

我先解释一下字符串的前缀和后缀。如果字符串A和B,存在A=BS,其中S是任意的非空字符串,那就称B为A的前缀。例如,”Harry”的前缀包括{”H”, ”Ha”, ”Har”, ”Harr”},我们把所有前缀组成的集合,称为字符串的前缀集合。同样可以定义后缀A=SB, 其中S是任意的非空字符串,那就称B为A的后缀,例如,”Potter”的后缀包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然后把所有后缀组成的集合,称为字符串的后缀集合。要注意的是,字符串本身并不是自己的后缀。

有了这个定义,就可以说明PMT中的值的意义了。PMT中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度。例如,对于”aba”,它的前缀集合为{”a”, ”ab”},后缀 集合为{”ba”, ”a”}。两个集合的交集为{”a”},那么长度最长的元素就是字符串”a”了,长 度为1,所以对于”aba”而言,它在PMT表中对应的值就是1。再比如,对于字符串”ababa”,它的前缀集合为{”a”, ”ab”, ”aba”, ”abab”},它的后缀集合为{”baba”, ”aba”, ”ba”, ”a”}, 两个集合的交集为{”a”, ”aba”},其中最长的元素为”aba”,长度为3。

好了,解释清楚这个表是什么之后,我们再来看如何使用这个表来加速字符串的查找,以及这样用的道理是什么。如图 1.12 所示,要在主字符串"ababababca"中查找模式字符串"abababca"。如果在 j 处字符不匹配,那么由于前边所说的模式字符串 PMT 的性质,主字符串中 i 指针之前的 PMT[j −1] 位就一定与模式字符串的第 0 位至第 PMT[j−1] 位是相同的。这是因为主字符串在 i 位失配,也就意味着主字符串从 i−j 到 i 这一段是与模式字符串的 0 到 j 这一段是完全相同的。而我们上面也解释了,模式字符串从 0 到 j−1 ,在这个例子中就是”ababab”,其前缀集合与后缀集合的交集的最长元素为”abab”, 长度为4。所以就可以断言,主字符串中i指针之前的 4 位一定与模式字符串的第0位至第 4 位是相同的,即长度为 4 的后缀与前缀相同。这样一来,我们就可以将这些字符段的比较省略掉。具体的做法是,保持i指针不动,然后将j指针指向模式字符串的PMT[j −1]位即可。

简言之,以图中的例子来说,在 i 处失配,那么主字符串和模式字符串的前边6位就是相同的。又因为模式字符串的前6位,它的前4位前缀和后4位后缀是相同的,所以我们推知主字符串i之前的4位和模式字符串开头的4位是相同的。就是图中的灰色部分。那这部分就不用再比较了。

 

有了上面的思路,我们就可以使用PMT加速字符串的查找了。我们看到如果是在 j 位 失配,那么影响 j 指针回溯的位置的其实是第 j −1 位的 PMT 值,所以为了编程的方便, 我们不直接使用PMT数组,而是将PMT数组向后偏移一位。我们把新得到的这个数组称为next数组。下面给出根据next数组进行字符串匹配加速的字符串匹配程序。其中要注意的一个技巧是,在把PMT进行向右偏移时,第0位的值,我们将其设成了-1,这只是为了编程的方便,并没有其他的意义。在本节的例子中,next数组如下表所示。

int KMP(char * t, char * p) 
{
	int i = 0; 
	int j = 0;
 
	while (i < strlen(t) && j < strlen(p))
	{
		if (j == -1 || t[i] == p[j]) 
		{
			i++;
           		j++;
		}
	 	else 
           		j = next[j];
    	}
 
    if (j == strlen(p))
       return i - j;
    else 
       return -1;
}

好了,讲到这里,其实KMP算法的主体就已经讲解完了。你会发现,其实KMP算法的动机是很简单的,解决的方案也很简单。远没有很多教材和算法书里所讲的那么乱七八糟,只要搞明白了PMT的意义,其实整个算法都迎刃而解。

现在,我们再看一下如何编程快速求得next数组。其实,求next数组的过程完全可以看成字符串匹配的过程,即以模式字符串为主字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。

具体来说,就是从模式字符串的第一位(注意,不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。如下图所示。

求next数组值的程序如下所示:

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

 注:第一部分的实现的最开始位置是从 j  = -1开始的,初始值为 -1 字符串的起始位置为0 这个和教材中的不一样

(2)第二部 教材中的KMP算法以及其改进

//串的存储结构的定义
typedef  struct _String
{

char str[MAX_Size +1];
 int  length;

    
} Str;

//KMP算法的实现

void getnext( Str substr, int *next)
{
    int i= 1 ,j=0;
    next[1] = 0;
    while(i< substr.length)
    {

        if(j==0 || substr.str[i] == substr.str[j])
        {
            ++i;
            ++j;
            next[i] = j;
        }
        else
        {
            j = next[j];
        }
    }
}
int KMP(Str str, Str substr,int* next)
{
    int i = 1 , j = 1;
    while(i<=str.length&& j<substr.length )
    {
        if(j==0|| str.str[i] == substr[j])
        {
         ++i;
         ++j;
        }
        else
        j = next[j];//调整子串的比较位置
    }
    if(j>substr.length)
    {
        return i - substr.length;
        
    }
    else
    return 0;
}

//关于KMP算法的改进 --减少了特殊情况下的比较次数 时间复杂度为O(n +m)
int getnextval(Str substr ,int nextval[])
{
int i= 1, j = 0;
nextval[1] = 0;
while(i<substr.length)
{
    if(j==0 || substr.str[i] == substr.str[j])
    {   
        ++i;
        ++j;
        if(substr.str[i] != substr.str[j])
         nextval[i] = j;
         else
         nextval[i] = next[j];
    }
    else
    j = nextval[j];
}
}

 

 

 

 

(3)第三部分 另外一种更容易懂的KMP算法实现

void next(char *str, int *next, int length)
{
    next[0] = -1;
    int k = -1;
    for (int q = 1; q <= length-1; q++)//字符位置
    {
        while (k > -1 && str[k + 1] != str[q])
        {//K是最长公共前缀的最后一个字符的位置 在加一个
            k = next[k];
//往前回溯 next[k]中保存的是当前公共子串的公共子串的公共前缀的最后一个位置
        }
        if (str[k + 1] == str[q])//如果在公共前缀和后缀后面同时增加一个字符相同,k++
        {
            k = k + 1;//当前子串的最长公共子串的最后一个字符
        }
        next[q] = k;//保存当前子串的公共子串的位置
    }
}

[这段代码:主要就是回溯规则的确定,用一句话来说明就是从已经有的公共最长前缀和后缀中分别在前后加一个字符,如果这个不满足,则继续按照前述规则进行比较 直到满足条件或者 -1]

具体分析,这个核心函数用于计算next数组的值,next数组的值的含义是什么呢,就是从第一个字符开始到当前字符为止的字符的相同的最长前缀最长后缀长度。(这里要特别说明的是所谓的最长前缀是不包含最后一个字符的,最长后缀不包括第一个字符,否则将会出现没有意义的情况。一般谈最长前缀后缀都要至少两个字符的字符串,在这里,我们将只有一个字符的字符串的公共最长前缀和最长后缀定义为-1)

如:abcdefgabc,前缀和后缀相同的是abc,长度是3。next[i]储存的是str字符串中前i+1位字符串前缀和后缀的最长长度。如abadefg,next[2]=1存的是aba这个字符串前缀和后缀的最长长度。

这里用一个比较明显的字符串abababac来做例子,先创建一个和字符串长度相同的数组next。第一位设为-1。

1)串a

https://i-blog.csdnimg.cn/blog_migrate/bf49491bf24f0dc6517c60db9f3b754c.png

 

2)所以向后移一位开始比较 ab

https://i-blog.csdnimg.cn/blog_migrate/5a0d96b3dc31c4dfc6af437cd6023674.png

 

a和b不同,next第二位写-1

(3)串aba

 

再向后移一位

https://i-blog.csdnimg.cn/blog_migrate/d60be3dbe46a438c9bc0d4d8346cd634.png

 

a和a相同,next数组存前一个k=-1加1等于0.

https://i-blog.csdnimg.cn/blog_migrate/71d0222fdefc4149d3a27fc6e659a365.png

 

(4)串abab

再比较下一位,相同就比前一个加一。

 

https://i-blog.csdnimg.cn/blog_migrate/72714c70ed216deccd86029996d20005.png

到第7位时,cb不再相等,这时就用到了回溯!!!先看下一次比较时,应该移动到哪里

(这里其实就是用一句话就可以说明:原来的情况是ababa是最长公共前后缀的串,现在加了一个字符,要比较的就是ababa所在的最大前缀的最后一个字符的下一位 和最长后缀的最后一个字符的下一位 看看是否可以增加子串的长度 ababab和ababac 这个不行 回溯, 找到上一个最长公共前缀的最后一个字符 也就是aba 再分别加一个字符 abab 和abac 这个依旧不相等 继续向前回溯,这个时候的最长公共前缀的a)

https://i-blog.csdnimg.cn/blog_migrate/e3d15fa273658bb50560a61a31952ccc.png

 

原因要再回来看上一次比较,上一次比较相同长度为5。

https://i-blog.csdnimg.cn/blog_migrate/c764241c85f131ae5de47e6fbe09dec0.png

 

看这5个字符串相同的前后缀的长度,即next[4]中储存的值:2再加1,因为这5个字符串就是str的前五个字符串!

https://i-blog.csdnimg.cn/blog_migrate/d8b202a57c4afbe57dedcbf17e5f181a.png

所以k先回溯到2,再比较下一个字符是否相同,这里是比较c和b,不同,再回溯,这次前面剩下aba三个字符,k=next[2]=0,即前后缀还有一个字符相同,所以应该后移到下图位置。

https://i-blog.csdnimg.cn/blog_migrate/47db8552c7eca5ab80adfa3bc1427afb.png

 

再比较c和b,还不同,再回溯,k=next[0]=-1,然后next[7]=-1,这样就求出了next数组了。

https://i-blog.csdnimg.cn/blog_migrate/5ac629321655e683f46cef4e11b98280.png

//具体代码:
int KMP(char *str, int slen, char *ptr, int plen)
{
    int *next = new int[plen];
    cal_next(ptr, next, plen);//计算next数组
    int k = -1;
    for (int i = 0; i < slen; i++)
{
	//子串与母串进行匹配
        while (k >-1&& ptr[k + 1] != str[i])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配)
            k = next[k];//往前回溯
        if (ptr[k + 1] == str[i])
            k = k + 1;
        if (k == plen-1)//说明k移动到ptr的最末端
        {
            //cout << "在位置" << i-plen+1<< endl;
            //k = -1;//重新初始化,寻找下一个
            //i = i - plen + 1;//i定位到该位置,外层for循环i++可以继续找下一个(这里默认存在两个匹配字符串可以部分重叠),感谢评论中同学指出错误。
            return i-plen+1;//返回相应的位置
        }
    }
    return -1;  
}
// 这个算法的实质就是通过快速确定子串和母串的比较位置,来确定是否匹配。(通常的做法是子串不回溯,回溯的是父串 KMP的做法是正好相反)

 这个地方相当的重要,在以后的学习过程中,还是需要加强巩固和学习。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值