前言
这篇帖子是我个人在学习KMP算法获取next数组这部分代码时形成的思考,在浏览了诸多帖子以后,发现对于k = next[k]这行代码的逻辑还是没有清晰的认知,因此想自己写一篇帖子理顺自己的思路,主要是借此平台做学习笔记,如果可以帮助到别人那自然是极好的,也希望有大佬可以指教。
这篇帖子不探讨KMP算法详细的实现原理,只关注通过模式串得到next数组的逻辑,特别是k = next[k] 这行代码的逻辑。
关于KMP算法整体的讲解我参考的是这篇文章:很详尽KMP算法(厉害) - ZzUuOo666 - 博客园 (cnblogs.com)
int* get_next(string s) {
int len = int(s.length());
int *next = new int[len]();
int j = 0, k = -1;
next[0] = -1;
while(j < len - 1) {
if(k == -1 || s[j] == s[k]) {
++j; ++k;
next[j] = k;
} else {k = next[k];}
}
return next;
}
一、next数组的基本意义
next[j] = k所表示的含义是字符串中下标为j的字符之前(不包括该字符),能匹配的前缀子串以及后缀子串的最大长度是k,通俗来讲,就是当模式串匹配到下标为[j]的元素时如果失配了,可以从下标为[k]的元素开始进行比对,[k]之前的元素已经匹配了。
注意:以上思考是针对我在前言中所贴的代码而言的,不同的代码可能会有不同理解。
二、获取next数组值的基本逻辑
对于长度大于1的匹配的前后缀,都是在已经匹配的前后缀的基础上,对前缀的后一位以及后缀的后一位进行比较,如果两个后一位的元素相同,则找到了新的匹配的前后缀。这一点和KMP算法的核心匹配逻辑是相同的(毕竟核心代码逻辑都一样,都是x = next[x])。
三、k = next[k],即指针回退的具体逻辑
k = next[k]针对两种情况,一种是没有匹配的前缀和后缀,即s[k] 与 s[j]始终不相等,指针回退至“-1”状态,然后下一个循环符合if判断语句中“k == -1”的语句,“++k; ++j”,然后给next[j+1]赋值0,这种情况比较好理解,不做详细阐述,用图像模拟函数每个循环。
该图像中前三行分别是字符串元素、字符串下标、下标对应next数组值,相同颜色代表相同的元素值,k与j是指向元素的指针,默认s[k] 与 s[j]始终不相等。
更重要的一种情况是,目前已经匹配的前缀与后缀长度为next[j](也就是当前k的值),但是s[j] != s[k],所以要尝试找到更短的已经匹配的前缀与后缀,如果有更短的匹配的前缀与后缀,且该前缀的后一个元素与后缀的后一个元素相同,那就找到了,那就将找到的前缀(后缀)长度赋值给next[j+1]。
下面就针对这种情况做具体分析:
首先通过图片模拟获取next数组时的一种情况,相同颜色代表相同的字符,方框内的深色数字即字符串下标,最下方的浅色数字代表下标对应的next数组的值,k与j是指针。
上图的模拟的情况是,程序运行至k = 5,j = 12,s[k] != s[j],此时next[12]的值已经求出,要求next[13]的值。根据此时的条件,因为k != -1 && s[k] != s[j],所以程序会进入else条件语句,即运行k = next[k],具体运行逻辑我分几步进行梳理。
1、将数组分为两部分,一部分是已经匹配的前缀以及k所指的元素,另一部分是已经匹配的后缀以及j所指的元素。
已经明确的是,next数组值的含义是某个元素之前(不包括该元素)能匹配的前缀子串以及后缀子串的最大长度,所以此时next[k] = 3就是s[k]之前能匹配的前缀与后缀的长度为3,因为除了“k == -1”,只有当“s[k] == s[j]”时,程序才会执行“++i; ++j”,且找不到匹配的前后缀时k会回退至“-1”,所以s[k]之前的元素与s[j]之前的k个元素是相同的,这点通过观察图像也可以很清晰的知道,因此,假如s[k]之前(即s[0]至s[k-1])的字符串有可以匹配的前后缀,那s[j]之前的k个元素(即s[j-k]至s[j-1])形成的字符串必然有相同的前后缀。
2、将找到的更短的符合条件的前缀与后缀分开表示
运行了k = next[k]后,k的值变为3,此时k指向的就是找到的更短的已经匹配的前缀的后一个元素,前缀与后缀分开表示,这时候就不难发现,k与j所指的元素相同,因此就找到了新的符合条件的前后缀,即符合“s[k] == s[j]”。
3、将字符串按原来的样式表示
此时已经执行if语句,k与j的值都已经加一,并且对next[j]赋值找到的符合条件的前后缀长度:4。
至此,整个get_next函数执行完毕。