这里写目录标题
-
- KMP数组计算方式
-
- **问题描述**
- **初始准备**
- **逐步推导过程**
-
- [**Step 1: `i = 1`,子串为 `ab`**](#Step 1:
i = 1
,子串为ab
) - [**Step 2: `i = 2`,子串为 `aba`**](#Step 2:
i = 2
,子串为aba
) - [**Step 3: `i = 3`,子串为 `abab`**](#Step 3:
i = 3
,子串为abab
) - [**Step 4: `i = 4`,子串为 `ababa`**](#Step 4:
i = 4
,子串为ababa
) - [**Step 5: `i = 5`,子串为 `ababac`**](#Step 5:
i = 5
,子串为ababac
) - [**Step 6: `i = 6`,子串为 `ababaca`**](#Step 6:
i = 6
,子串为ababaca
)
- [**Step 1: `i = 1`,子串为 `ab`**](#Step 1:
- **完整结果**
- **关键步骤总结**
- **代码一步步实现**
- **运行结果展示**
- **总结**
- [**匹配失败时 核心思路**](#匹配失败时 核心思路)
KMP数组计算方式
c
#include <iostream>
#include <vector>
using namespace std;
void computeNext(const string& pattern) {
int n = pattern.size();
vector<int> next(n, 0); // 初始化 next 数组
int j = 0; // 前缀指针
cout << "Step-by-step explanation:" << endl;
for (int i = 1; i < n; ++i) {
// 处理回退
while (j > 0 && pattern[i] != pattern[j]) {
cout << "Mismatch: i=" << i << ", j=" << j
<< " -> backtrack j=next[" << (j - 1) << "]=" << next[j - 1] << endl;
j = next[j - 1];
}
// 匹配成功
if (pattern[i] == pattern[j]) {
j++;
}
next[i] = j;
// 输出当前步骤
cout << "i=" << i << ", j=" << j << ", next[" << i << "]=" << next[i] << endl;
}
// 输出完整的 next 数组
cout << "Final next array: ";
for (int x : next) {
cout << x << " ";
}
cout << endl;
}
int main() {
string pattern = "ababaca";
computeNext(pattern);
return 0;
}
我们一步步详细讲解如何求 next
数组,以最直观的方式拆解过程。
问题描述
给定模式串:ababaca
目标:计算出每个位置 i
的 next[i]
,即从开头到位置 i
子串的最长相同前后缀的长度。
初始准备
- 模式串长度: 模式串为
ababaca
,长度为 7。 next
数组: 初始化为[0, 0, 0, 0, 0, 0, 0]
。next[0] = 0
,因为第一个字符没有前后缀。
我们从模式串的第 1 个字符开始(i=1
),逐步推导 next
数组。
逐步推导过程
Step 1: i = 1
,子串为 ab
- 比较
pattern[1]
(b
) 和pattern[0]
(a
)。 - 不相同,所以没有前后缀,
next[1] = 0
。
当前 next
数组:[0, 0, 0, 0, 0, 0, 0]
Step 2: i = 2
,子串为 aba
- 比较
pattern[2]
(a
) 和pattern[0]
(a
)。 - 相同,说明前后缀延续,最长前后缀长度为 1。
- 更新:
next[2] = 1
。
当前 next
数组:[0, 0, 1, 0, 0, 0, 0]
Step 3: i = 3
,子串为 abab
- 比较
pattern[3]
(b
) 和pattern[1]
(b
)。 - 相同,说明前后缀延续,最长前后缀长度为 2。
- 更新:
next[3] = 2
。
当前 next
数组:[0, 0, 1, 2, 0, 0, 0]
Step 4: i = 4
,子串为 ababa
- 比较
pattern[4]
(a
) 和pattern[2]
(a
)。 - 相同,说明前后缀延续,最长前后缀长度为 3。
- 更新:
next[4] = 3
。
当前 next
数组:[0, 0, 1, 2, 3, 0, 0]
Step 5: i = 5
,子串为 ababac
- 比较
pattern[5]
(c
) 和pattern[3]
(b
)。 - 不相同,需要回退。
- 回退到
next[j-1] = 1
,再比较pattern[5]
和pattern[1]
(b
)。 - 仍然不相同,再回退到
next[j-1] = 0
, while (j > 0 && pattern[i] != pattern[j]) 结束。 pattern[5]
和pattern[0]
(a
)仍然不相同,最终j = 0
,此时没有前后缀。- 更新:
next[5] = 0
。
当前 next
数组:[0, 0, 1, 2, 3, 0, 0]
Step 6: i = 6
,子串为 ababaca
- 比较
pattern[6]
(a
) 和pattern[0]
(a
)。 - 相同,最长前后缀长度为 1。
- 更新:
next[6] = 1
。
当前 next
数组:[0, 0, 1, 2, 3, 0, 1]
完整结果
最终 next
数组为:[0, 0, 1, 2, 3, 0, 1]
关键步骤总结
- 匹配成功时: 如果
pattern[i] == pattern[j]
,前后缀匹配,更新next[i] = j + 1
,并继续处理下一个字符。 - 匹配失败时:
- 回退到前一个可能的前缀位置(
j = next[j-1]
),直到找到可以匹配的前缀,或者回退到j = 0
。
- 回退到前一个可能的前缀位置(
- 初始化:
next[0] = 0
,第一个字符没有前后缀。
代码一步步实现
cpp
#include <iostream>
#include <vector>
using namespace std;
void computeNext(const string& pattern) {
int n = pattern.size();
vector<int> next(n, 0); // 初始化 next 数组
int j = 0; // 前缀指针
cout << "Step-by-step explanation:" << endl;
for (int i = 1; i < n; ++i) {
// 处理回退
while (j > 0 && pattern[i] != pattern[j]) {
cout << "Mismatch: i=" << i << ", j=" << j
<< " -> backtrack j=next[" << (j - 1) << "]=" << next[j - 1] << endl;
j = next[j - 1];
}
// 匹配成功
if (pattern[i] == pattern[j]) {
j++;
}
next[i] = j;
// 输出当前步骤
cout << "i=" << i << ", j=" << j << ", next[" << i << "]=" << next[i] << endl;
}
// 输出完整的 next 数组
cout << "Final next array: ";
for (int x : next) {
cout << x << " ";
}
cout << endl;
}
int main() {
string pattern = "ababaca";
computeNext(pattern);
return 0;
}
运行结果展示
对于模式串 ababaca
,逐步打印过程如下:
Step-by-step explanation:
i=1, j=0, next[1]=0
i=2, j=1, next[2]=1
i=3, j=2, next[3]=2
i=4, j=3, next[4]=3
Mismatch: i=5, j=3 -> backtrack j=next[2]=1
Mismatch: i=5, j=1 -> backtrack j=next[0]=0
i=5, j=0, next[5]=0
i=6, j=1, next[6]=1
Final next array: 0 0 1 2 3 0 1
总结
这种逐步回退+匹配的方法能有效构建 next
数组,时间复杂度为 (O(n)),非常高效!
j = next[j - 1]
是 KMP 算法求解 next
数组的核心逻辑,用于在字符不匹配时 回退 前缀指针 j
,快速找到新的匹配位置。
匹配失败时 核心思路
next[j-1]
存储的是从pattern[0..j-1]
这段子串的最长相同前后缀的长度。- 回退到
next[j-1]
,表示将j
移动到这个较短前缀的末尾,从该位置重新尝试匹配。
直观解释
假设当前已经匹配了 j
个字符(即 pattern[0..j-1]
),但下一个字符 pattern[i]
不匹配 pattern[j]
:
情况 1:暴力回退
- 如果不用
next
数组,匹配失败时只能暴力将j
回到 0,重新开始匹配,效率较低。
情况 2:利用 next
回退
next[j-1]
告诉我们:模式串pattern[0..j-1]
中,最长相同前后缀的长度是next[j-1]
。- 因此,直接将
j
移动到next[j-1]
,相当于跳过一部分前缀字符,避免重复比较。 - 从
pattern[next[j-1]]
开始尝试匹配,能快速继续匹配,而不需要回退到开头。
举例分析
模式串:ababaca
我们计算 next
数组时,假设当前:
- ( i = 5 ),即正在处理
pattern[5]
(c
)。 - ( j = 3 ),表示之前已经匹配了 3 个字符(前缀
aba
)。
此时,pattern[5] != pattern[3]
,匹配失败。
如何回退?
j = 3
时,之前匹配的子串是aba
,其最长相同前后缀长度为next[3-1] = next[2] = 1
。- 意味着,前缀
a
和后缀a
是匹配的。
- 意味着,前缀
- 所以,直接将
j
移动到位置1
(next[2]
的值),尝试从更短的前缀a
开始重新匹配pattern[i] = c
。
结果:
pattern[i]
仍然不匹配(pattern[5] != pattern[1]
),继续回退j = next[j-1] = next[0] = 0
。- 最终回退到
j = 0
,表示无法找到更短的前缀可以匹配。