题干
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1。
题解
最简单的方法是暴力,假设主串指针为i,模式串指针为j,当haystack[i] == needle[j],就将i和j同时向后移动,如果haystack[i] != needle[j],就将i和j都回溯,再次尝试匹配。假设haystack的长度为m,needle的长度为n,这个算法的时间复杂度为O(m*n)。暴力法时间复杂度高的主要原因是因为当每次主串和模式串失配的时候,主串指针需要回溯到下一个可能的模式串开始位置,模式串指针需要回溯到开头,kmp算法可以保证当出现失配的时候,主串指针不回溯,且模式串指针不需要每次都回溯到头部,从而优化了时间复杂度。
在kmp算法中,在未出现失配的情况下,处理方式和暴力算法一致,区别就在于失配情况下的处理。思考这样一个问题,如果我们不回溯主串的指针,下一个和主串指针位置上的字母比较的模式串字母应该是哪一个?。当haystack[i] != needle[j]时,我们知道当前位置失配了,同时也知道,i-j到i位置上的所有字母都匹配上了,既然这个范围内的所有字母都和模式串一致,我们就可以认为haystack[(i-j):i]是模式串的子串,当模式串的子串拥有相同的最长前缀和最长后缀的时候,我们就可以移动模式串,使得模式串的最长前缀,对齐主串的最长后缀,这个时候这个时候j指向的字母就是下一个要和主串比较的字母。举个例子,现在有主串abeababeabd,模式串abeabd,当遍历到a和d的时候出现了失配,但是我们发现模式串的子串abeab的前缀ab和后缀ab是一致的,所以我们可以把模式串的前缀ab对齐主串的后缀ab,下一个比较的就是主串的a和模式串的e;而a和e又出现了失配,这时模式串的子串ab没有相等的前后缀,所以模式串回溯到开头重新开始匹配。
以上就是kmp算法的核心思想,在实际实现中,我们定义一个next数组,next[i]表示needle[0:i]中前后缀相等的最大长度,next数组的作用是当在模式串的i位置出现失配的时候,用于计算下一个匹配的模式串位置,而根据上面的分析,当出现失配的时候,主串一定有和模式串一致的子串,那么这个子串就可以被看做是模式串的子串,因此next数组的计算就和主串没有关系,只和模式串有关系。next数组的计算方法如下:
- 设置两个指针
i和j,i指向前缀的尾部,j指向后缀的尾部,初始值分别为1和0 - 当
needle[i] == needle[j]时,代表前缀和后缀匹配上了,next[i] = next[i-1] + 1 - 当
needle[i] != needle[j]时,代表前缀和后缀没有匹配上,则不断使j = next[j-1]循环向前寻找一个j使得needle[j] == needle[i],如果到头还没找到,则next[i] = 0代表没有相等的前后缀。 在计算得到next数组之后,用同样的思路进行主串和模式串的匹配,当出现失配的时候,不断使j = next[j-1]循环向前寻找一个j使得haystack[i] == needle[j],目的是为了使得模式串的前缀能够对上主串的后缀,如果循环到模式串头部还没找到,就意味着模式串回溯到头了。直到j指向了模式串的尾部,代表在主串中找到了模式串,返回i-j;否则返回-1。
func strStr(haystack string, needle string) int {
m, n := len(haystack), len(needle)
next := make([]int, n)
// 构造next数组
for i, j := 1, 0; i < n; i++ {
for j > 0 && needle[j] != needle[i] {
j = next[j-1]
}
if needle[i] == needle[j] {
j++
}
next[i] = j
}
// 匹配模式串和主串,i为主串指针,j为模式串指针
for i, j := 0, 0; i < m; i++ {
// 当出现失配的情况,移动模式串指针寻找模式串和主串比较的下一个位置
for j > 0 && haystack[i] != needle[j] {
j = next[j-1]
}
// 当匹配上了,将主串指针和模式串指针都向后移动,否则只移动主串指针,主串指针不回溯
if haystack[i] == needle[j] {
j++
}
// 当遍历完模式串,代表模式匹配成功,返回模式串在主串中的起始位置
if j == n {
return i - n + 1
}
}
return -1
}
算法的时间复杂度为O(m+n),空间复杂度为O(n)