一、什么是最长公共前后缀
最长公共前缀:字符串中不包含最后一个字符,必须包含第一个字符的连续子串。
最长公共后缀:字符串中不包含第一个字符,必须包含最后一个字符的连续子串。
二、什么是部分匹配表next数组
next 数组是用于 KMP 算法的核心辅助数组,用于加速匹配。next数组就是模式串从第一个字符开始到当前位置字符构成的连续字符串的最长公共前后缀的长度的集合。
next[i]就是从模式串的第一个字符开始到模式串当前位置字符构成的连续字符串的最长公共前后缀。
举个例子
模式串:pattern = "ababaca"
1、在规定初始时next[0] = 0的计算步骤
i = 0,pattern[0] = a
子串:a
前缀:空
后缀:空
公共前后缀:无
next[0] = 0
i = 1,pattern[1] = b
子串:ab
前缀:a
后缀:b
公共前后缀:无
next[1] = 0
i = 2,pattern[2] = a
子串:aba
前缀:a, ab
后缀:a, ba
公共前后缀:a
next[2] = 1(最长公共前后缀为 a)
i = 3,pattern[3] = b
子串:abab
前缀:a, ab, aba
后缀:b, ab, bab
公共前后缀:ab
next[3] = 2(最长公共前后缀为 ab)
i = 4,pattern[4] = a
子串:ababa
前缀:a, ab, aba, abab
后缀:a, ba, aba, baba
公共前后缀:aba
next[4] = 3(最长公共前后缀为 aba)
i = 5,pattern[5] = c
子串:ababac
前缀:a, ab, aba, abab, ababa
后缀:c, ac, bac, abac, babac
公共前后缀:无
next[5] = 0(没有公共前后缀)
i = 6,pattern[6] = a
子串:ababaca
前缀:a, ab, aba, abab, ababa, ababac
后缀:a, ca, aca, baca, abaca, babaca
公共前后缀:a
next[6] = 1(最长公共前后缀为 a)
所以在next[0] = 0时,next数组是[0,0 ,1, 2, 3, 0, 1]
2、另外,还有一种规定初始时next[0] = -1
此时计算next[i]的值,当实际上是根据pattern[0:i-1]的子串计算出的最长公共前后缀。
i = 0,pattern[0] = a
子串:空
前缀:空
后缀:空
公共前后缀:无
next[0] = -1
i = 1,pattern[1] = b
子串:a
前缀:空
后缀:空
公共前后缀:无
next[1] = 0
i = 2,pattern[2] = a
子串:ab
前缀:a
后缀:b
公共前后缀:空
next[2] = 0(最长公共前后缀为 a)
i = 3,pattern[3] = b
子串:aba
前缀:a, ab
后缀:a, ba
公共前后缀:a
next[3] = 1(最长公共前后缀为 a)
i = 4,pattern[4] =a
子串:abab
前缀:a, ab, aba
后缀:b, ab, bab
公共前后缀:ab
next[4] = 2(最长公共前后缀为 ab)
i = 5,pattern[5] =c
子串:ababa
前缀:a, ab, aba, abab
后缀:a, ba, aba, baba
公共前后缀:aba
next[5] = 3(最长公共前后缀为 aba)
i = 6,pattern[6] = a
子串:ababac
前缀:a, ab, aba, abab, ababa
后缀:c, ac, bac, abac, babac
公共前后缀:无
next[6] = 0(没有公共前后缀)
所以在next[0] = -1时,next数组是[-1,0,0 ,1, 2, 3, 0]
观察计算过程和结果可以看出,其实next[0]=-1求出的next数组[-1,0,0 ,1, 2, 3, 0]就是next[0]=0求出的next数组[0,0 ,1, 2, 3, 0,1] 下标整体后移一位,最后面的一位丢弃,并在第一位补-1得到的。通常我们编码中规定next[0]=0即可。
3、c++代码给定模式串求next数组
假设模式串为 pattern,长度为 n。我们从头到尾依次求出每个位置的 next 值。
初始化:next[0] = 0,因为第一个字符没有前缀或后缀。
递推:从 i = 1 开始,求解 next[i]。
定义 j 为当前已经匹配的最长前缀的长度(即 pattern[0...j - 1] 匹配了 pattern[i - j...i - 1]),初始 j = 0。
若 pattern[i] == pattern[j],则 next[i] = j + 1,表示当前的前缀长度可以扩展。
若 pattern[i] != pattern[j],则根据 next[j] 回退 j,直到 j == 0 或找到新的匹配位置。
//next[0]= 0时
void computeNext(const std::string &pattern, std::vector<int> &next) {
int n = pattern.length();
next[0] = 0; // 初始化
int j = 0; // 已匹配的前缀长度
for (int i = 1; i < n; i++) {
// 如果不匹配,回退到 next[j-1]
while (j > 0 && pattern[i] != pattern[j]) {
j = next[j - 1];
}
// 如果匹配,前缀长度增加
if (pattern[i] == pattern[j]) {
j++;
}
next[i] = j;
}
}
//next[0]= -1时同理
void computeNext(const std::string &pattern, std::vector<int> &next) {
int n = pattern.length();
next[0] = -1; // 初始化
int j = -1; // 已匹配的前缀长度
for (int i = 1; i < n; i++) {
// 如果不匹配,回退到 next[j]
while (j >= 0 && pattern[i] != pattern[j + 1]) {
j = next[j];
}
// 如果匹配,前缀长度增加
if (pattern[i] == pattern[j + 1]) {
j++;
}
next[i] = j;
}
}
对于模式串ababaca来模拟一下递推过程
初始next[0]=0,j=0从i=1开始递推,此时子串ab,pattern[i]=b, pattern[i] 不等于pattern[j],但j不大于0所以没有必要回退,那么next[1]=j=0。
当i=2时,此时子串aba,pattern[i]=a,j=0,pattern[i] 等于pattern[j],则表示当前位置前缀长度可以增加1,即j=1,next[2]=j=1。
当i=3时,此时子串abab,pattern[i]=b,j=1,pattern[i] 等于pattern[j],则表示当前位置前缀长度可以增加1,即j=2,next[3]=j=2。
当i=4时,此时子串ababa,pattern[i]=a,j=2,pattern[i] 等于pattern[j],则表示当前位置前缀长度可以增加1,即j=3,next[4]=j=3。
当i=5时,此时子串ababac,pattern[i]=c,j=3,pattern[i] 不等于pattern[j],但j>0,所以要回退j。要回退多少呢?是要根据next数组回退到next[j-1]。为什么根据next数组回退,为什么回退到next[j-1]呢?因为next数组本身存储的就是最长的前缀长度,当i=5时,j=3,pattern[i] 不等于pattern[j],即abab的b不等于abac的c,但是 我们可以先回退到相等子串aba时的最长公共前后缀长度1,即j=next[2]=1,然后再来判断 pattern[i] 还是不等于pattern[j] ,此时对比的是ab的b和ac和c,还需要回退到相等子串a的最长公共前后缀0,即j=next[0]=0,此时的j不大于0,就是说没有公共前后缀所以没有必要回退。next[5]=j=0。
当i=6时,此时子串ababaca,pattern[i]=a,j=0,pattern[i] 等于pattern[j],则表示当前位置前缀长度可以增加1,即j=1,next[6]=j=1。
可得出next数组[0,0 ,1, 2, 3, 0,1]
三、什么是nextval数组
nextval数组是对 next 数组的改进,它进一步优化了模式串的移动。当发现 pattern[i] == pattern[next[i]] 时,如果直接使用 next[i],则有可能产生多余的比较,因为 pattern[i] 和 pattern[next[i]] 已经相同了。nextval 解决了这个问题,通过直接跳过这些已经相等的部分。
c++代码给定模式串、next数组、求nextval数组
初始化:nextval[0] = -1 或 nextval[0] = 0(根据不同实现风格)。
对于每个 i,若 pattern[i] == pattern[next[i]],则 nextval[i] = nextval[next[i]];否则 nextval[i] = next[i]。
解释一下什么意思呢?
nextval[0]等于next[0];
以next的值充当模式串的下标取得对应字符,当同一下标下模式串的字符与取得的字符相同时,nextval当前下标的值就是nextval以next当前下标取得的值为下标所取的值。如果不同时则nextval当前下标的值就是next当前下标取得的值。
//next[0]= 0时
void computeNextVal(const std::string &pattern, std::vector<int> &next, std::vector<int> &nextval) {
int n = pattern.length();
nextval[0] = 0; // 初始化与 next 相同
for (int i = 1; i < n; i++) {
if (pattern[i] == pattern[next[i]]) {
nextval[i] = nextval[next[i]];
} else {
nextval[i] = next[i];
}
}
}
//next[0] =-1时
void computeNextVal(const std::string &pattern, std::vector<int> &next, std::vector<int> &nextval) {
int n = pattern.length();
nextval[0] = -1; // 初始化与 next 相同
for (int i = 1; i < n; i++) {
// 确保 next[i] >= 0,以防止数组越界
if (next[i] >= 0 && pattern[i] == pattern[next[i]]) {
nextval[i] = nextval[next[i]];
} else {
nextval[i] = next[i];
}
}
}
四、BF算法(暴力匹配)
BF算法(Brute Force,又称暴力匹配算法)是最简单、直接的字符串匹配算法。
逐字符匹配:从主串的第一个字符开始,尝试逐字符匹配模式串。如果匹配失败,则将模式串向右移动一个字符,重新开始匹配,直到找到完整匹配或主串遍历结束。
暴力搜索:BF算法不做任何优化,依次检查主串的每一个可能的位置,看模式串是否匹配。
假设主串是 S,长度为 n,模式串是 P,长度为 m。
从主串 S 的第一个字符开始,逐个字符地检查是否与模式串 P 的第一个字符匹配。
如果当前字符匹配,则继续比较后面的字符。
如果当前字符不匹配或匹配到最后一个字符:
如果匹配成功,记录下当前的匹配位置。
如果匹配失败,将模式串向右移动一个字符,重新从头开始匹配模式串。
继续上述步骤,直到主串的剩余字符长度小于模式串时停止。
c++代码实现
#include <iostream>
#include <string>
using namespace std;
int BF(const string& S, const string& P) {
int n = S.size();
int m = P.size();
// 遍历主串 S 的所有可能匹配的起始位置
for (int i = 0; i <= n - m; i++) {
int j = 0;
// 尝试匹配 P 和 S 中的每个字符
while (j < m && S[i + j] == P[j]) {
j++;
}
// 如果 j 达到 m,说明找到了匹配
if (j == m) {
return i; // 返回匹配的起始位置
}
}
return -1; // 若未找到匹配,返回 -1
}
int main() {
string S = "hello world";
string P = "world";
int pos = BF(S, P);
if (pos != -1) {
cout << "Pattern found at index: " << pos << endl;
} else {
cout << "Pattern not found." << endl;
}
return 0;
}
五、KMP算法
KMP算法(Knuth - Morris - Pratt算法)是用于在字符串中快速查找子串的经典算法。它的关键在于通过预处理模式串生成一个 next数组,帮助在匹配失败时避免重复的字符比较,从而提高匹配效率。
核心思想:
避免重复比较:当匹配失败时,KMP算法通过 next 数组决定模式串需要回退到哪个位置,而不是从头重新匹配。所以重点还是计算next数组。
部分匹配表(next数组):next[i] 记录了在模式串中,从 0 到 i 的子串中,前缀和后缀相等的最长公共前后缀的长度。
KMP算法的流程:
构建 next 数组:首先,根据模式串的部分匹配信息,构建 next 数组。
匹配过程:
将模式串与主串的字符逐一匹配。
当匹配失败时,通过 next 数组决定模式串的回退位置,而不是从头重新开始匹配。
当匹配成功时,继续比较下一个字符。
c++代码实现
//next[0] = 0时
void KMP(const std::string &text, const std::string &pattern) {
int m = text.size();
int n = pattern.size();
std::vector<int> next(n);
computeNext(pattern, next); // 生成 next 数组
int j = 0; // 模式串的位置
for (int i = 0; i < m; i++) {
// 匹配不成功时,模式串回退
while (j > 0 && text[i] != pattern[j]) {
j = next[j - 1];
}
// 匹配成功,移动到下一个字符
if (text[i] == pattern[j]) {
j++;
}
// 找到一个完整的匹配
if (j == n) {
std::cout << "Pattern found at index " << (i - j + 1) << std::endl;
j = next[j - 1]; // 回退以继续寻找下一个匹配
}
}
}
//next[0] =-1时
void KMP(const std::string &text, const std::string &pattern) {
int m = text.size();
int n = pattern.size();
std::vector<int> next(n);
computeNext(pattern, next); // 生成 next 数组
int j = -1; // 模式串的位置
for (int i = 0; i < m; i++) {
// 匹配不成功时,模式串回退
while (j >= 0 && text[i] != pattern[j + 1]) {
j = next[j];
}
// 匹配成功,移动到下一个字符
if (text[i] == pattern[j + 1]) {
j++;
}
// 找到一个完整的匹配
if (j == n - 1) {
std::cout << "Pattern found at index " << (i - j) << std::endl;
j = next[j]; // 回退以继续寻找下一个匹配
}
}
}
六、力扣28. 找出字符串中第一个匹配项的下标
显然可以这道题可以使用kmp算法,但是需要构建next数组不是很快捷。这里给出一种使用双指针的方式来解答问题。思路简单,方便快捷。
class Solution {
public:
int strStr(string haystack, string needle) {
int n = haystack.size(), m =needle.size();
for(int i =0;i<=n-m; i++){
int k =0,j = i;
while(k<m && haystack[j] == needle[k]){
j++;
k++;
}
if(k==m){
return i;
}
}
return -1;
}
};