KMP算法讲解(C++与python实现)
文章目录
KMP算法讲解
全称:Knuth-Morris-Pratt算法
KMP算法用于在一个文本串中高效地查找一个模式串的出现位置,它利用了模式串内部的重复性质,在匹配失败时避免不必要的比较,从而提高了匹配效率。
步骤及具体操作:
- 首先,我们要求出一个PM数组。
- 利用PM数组获取next数组:
next[] = [0]+PM[0:-1]
- 遍历主串和模式串:
- 如果两串其位置元素相等,主串位置++,模式串位置++
- 如果位置不等,模式串利用next数组回溯,直到相等或回到初始位,主串位置++
- 当模式串遍历完,代表找到匹配位置
- 若模式串未遍历完,主串剩余位置不足以匹配,则在主串中不存在。
PM数组
PM 数组:也称部分匹配表或失配函数,用于 KMP 算法中,在匹配失败时决定模式串向右移动的位置。
如何获取PM数组(从字符串表意分析):
- 首先我们明确前后缀的含义,前缀指除去最后一个字符的字符串,后缀指除去最前面一个字符的字符转,如:abab的前缀是aba,后缀是bab。
- 那么前后缀的子串又是什么呢?前缀从前往后,后缀从后往前。即aba的子串:
a,ab,aba
,后缀bab的子串:b,ab,bab
。 - 而PM数组就是在字符串该位置上,其前后缀最大公共子串的长度, 那么
abab
这个位置上就应该是2
,因为最大公共子串是ab
。 - 示例:ababa的PM数组就为[0,0,1,2,3]。
采用代码的形式获取PM数组:
- 我们使用j, i两个指针,i指针不回溯,j指针回溯
- 在求PM数组的过程中,我们同样要使用动态规划(匹配模式)的思想
- i指针指向1, j指针指向0
- 当str[i]== str[j] 匹配,i++, j++, PM[i] == j+1 (因为数组下标从0开始);
- 当str[i]!= str[j] 不匹配:
- 如果j > 0 ,利用已求得的PM数组回溯j指针
j = PM[j-1]
,直到j回到起始位,或者str[i]== str[j] ,则PM[i] = j +1, j++
; 这一步的思想在于:此时的j前面已经包含部分子串和i当前位置匹配,所以我们不用一一进行判断匹配 - 最后i++
- 如果j > 0 ,利用已求得的PM数组回溯j指针
- 当我们i走到最后一个字符,数组获取完成。
Cpp实现
void getNextArray(string pattern, vector<int> &nextVal, int strLen)
{
int i = 1, j = 0;
while (i < strLen)
{
if (pattern[i] == pattern[j])
{
nextVal[i] = j + 1;
j++;
}
else
{
while (j > 0)
{
j = nextVal[j - 1];
if (pattern[i] == pattern[j])
{
nextVal[i] = j + 1;
j++;
break;
}
}
}
i++;
}
nextVal.pop_back();
nextVal.insert(nextVal.begin(), -1);
}
next 数组
-
一个字符串的最长公共匹配值,前缀(除去最后一个字符)和后缀(除去第一个字符)相等的最长子串
-
如abaa的前缀aba 的子串有a、ab、aba,后缀baa的子串:a、aa、baa。
很明显,两者没有相等的子串,所以为0。
-
字符串 “ababa” 对应的 next 数组为:[-1, 0, 0, 1, 2]。
一些补充说明
- next数组是
模式串
的PM数组右移一位以后的数组(默认初始位为0) - PM数组是:前后缀最大相同数
- 利用代码寻找PM数组就是,设置一前一后两个指针
- j为初始位0,i是当前位置
- s[i]==s[j] n[i] == j+1 i++,j++
- s[i]!=s[j]
- j = n[j-1] 直到 s[j] == s[i] 然后同上
- 找到j = 0仍无法等于,n[i] = 0,j不变,i 后移
- KMP算法:
- 回溯的是子串,失配时根据next数组不断回溯,直到回无可回
- 主串永远向后移动,子串匹配或者回无可回就右移
- 时间复杂度为O(m+n),m,n分别为两字符串的长度
- 空间复杂度为O(m),构建了next数组
Leetcode实战演习
基础题例:Leetcode 28. 实现strStr()
python实现
- 代码(kmp算法代码):
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
'''
使用KMP算法实现
获取子串的next数组,
当位置失配时,就跳转到数组指定位置重新开始匹配
当第一位都第一位都失配,next[0] = -1
就移动主串的指针,重新开始匹配
'''
if not needle:
return 0
next = self.getNext(needle)
#主串起点,begin, 子串指针s son
b ,s = 0,0
while len(haystack) - b >= len(needle) - s:
#判断是否匹配,否则移动
while haystack[b] != needle[s] and s >= 0:
s = next[s]
#移动到匹配的地方了,匹配然后+1
if haystack[b] == needle[s]:
s += 1
# 没有匹配上,回到起点
else:
s = 0
if s == len(needle):
return b - len(needle) + 1
b += 1
return -1
- 代码2(next数组的寻找)
def getNext(self,s):
next = [0]*len(s)
#设置右左指针
i ,j = 1, 0
while i < len(s):
if s[i] == s[j]:
next[i] = j + 1
i += 1
j += 1
else:
while j > 0 :
j = next[j-1]
if s[i] == s[j]:
next[i] = j + 1
j += 1
break
i += 1
#现在只是PM数组,现在要右移一位
next = [-1] + next[:-1]
return next
C++实现
class Solution
{
public:
int strStr(string haystack, string needle)
{
// 使用KMP算法来解
if (needle.size() == 0)
{
return -1;
}
int ndSize = needle.size();
vector<int> nextVal(ndSize, 0);
// 获得nextval数组
getNextArray(needle, nextVal, ndSize);
// 初始化遍历位置。
int hsPos = 0, ndPos = 0;
int hsSize = haystack.size();
while (hsSize - hsPos >= ndSize - ndPos && ndPos < ndSize)
{
if (haystack[hsPos] == needle[ndPos])
{
ndPos++;
}
else
{
while (ndPos >= 0)
{
if (haystack[hsPos] == needle[ndPos])
{
ndPos++;
break;
}
else
{
ndPos = nextVal[ndPos];
}
}
if (ndPos < 0)
{
ndPos = 0;
}
}
hsPos++;
}
if (ndPos == ndSize)
{
return hsPos - ndSize;
}
return -1;
}
void getNextArray(string pattern, vector<int> &nextVal, int strLen)
{
int i = 1, j = 0;
while (i < strLen)
{
if (pattern[i] == pattern[j])
{
nextVal[i] = j + 1;
j++;
}
else
{
while (j > 0)
{
j = nextVal[j - 1];
if (pattern[i] == pattern[j])
{
nextVal[i] = j + 1;
j++;
break;
}
}
}
i++;
}
nextVal.pop_back();
nextVal.insert(nextVal.begin(), -1);
}
};