#5315. next数组笔记
next数组笔记
当前没有测试数据。
1. 先理解我们要干什么
我们有一个字符串s,比如"ababaca"。
- 对于位置
i(从0开始),我们要看子串s[0..i](即从开头到i的这段)中,最长的、既是前缀又是后缀、且长度小于子串本身的那一段有多长。 - 比如对于
i=4(子串"ababa"),前缀有"a"、"ab"、"aba"、"abab",后缀有"a"、"ba"、"aba"、"baba",其中相同的最长的是"aba",长度3,所以next[4]=3。
这个长度有什么用?在字符串匹配时,如果匹配到某个位置失败了,我们可以利用这个信息直接移动模式串,而不必从头开始比较。
2. 代码怎么工作的
代码中用两个指针i和j:
i从1开始,依次计算每个位置的next值。j是一个动态的“前缀长度”,同时也指向模式串中待比较的字符s[j]。
初始时,next[0]固定为0(第一个字符没有真前后缀)。然后我们开始循环:
for (int i = 1, j = 0; i < len; i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1]; // 关键回退
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
3. 关键思想:利用已知信息跳转
假设我们计算到了位置i,此时j是上一个位置i-1的next值,也就是说我们知道s[0..j-1](前缀)和s[i-j..i-1](紧挨着i前面的一段)是相等的。
现在我们想看看能不能把这个相等关系扩展到i位置:比较s[j]和s[i]。
- 如果相等,那就太棒了,直接
j++,说明最长前后缀长度增加了1。 - 如果不相等,我们就不能简单地延长了。但也许有一个更短的前缀仍然和
s[i]前面的部分匹配,并且这个更短的前缀的下一个字符恰好等于s[i]。这个更短的前缀长度怎么找?它就是next[j-1]。
为什么是next[j-1]?
因为我们已经知道s[0..j-1]和s[i-j..i-1]是相等的。那么s[0..j-1]这个子串本身也有自己的最长相等前后缀,记作next[j-1],设为k。这意味着s[0..k-1]等于s[j-k..j-1]。而s[j-k..j-1]又等于s[i-k..i-1](因为s[0..j-1]和s[i-j..i-1]整体相等,所以它们的尾部也相等)。所以s[0..k-1]就等于s[i-k..i-1]。也就是说,长度为k的前缀仍然和i前面的某一段匹配。于是我们可以把j缩小为k,再尝试比较s[k]和s[i]。
如果还不相等,就继续回退,直到j变成0,或者找到相等的字符。
这个过程就像是一个“递归”地利用前面已经算好的next值,不断寻找一个更短的、但依然匹配的前缀。
4. 举个例子:"ababaca"
我们用手算一遍,感受一下回退。
-
i=1, j=0:比较
s[1]='b'和s[0]='a',不相等,j不变,next[1]=0。
此时以i=1结尾的子串"ab"没有相同前后缀。 -
i=2, j=0:比较
s[2]='a'和s[0]='a',相等,j++变为1,next[2]=1。
子串"aba"的前缀"a"和后缀"a"相同,长度1。 -
i=3, j=1:比较
s[3]='b'和s[1]='b',相等,j++变为2,next[3]=2。
子串"abab"的前缀"ab"和后缀"ab"相同,长度2。 -
i=4, j=2:比较
s[4]='a'和s[2]='a',相等,j++变为3,next[4]=3。
子串"ababa"的前缀"aba"和后缀"aba"相同,长度3。 -
i=5, j=3:比较
s[5]='c'和s[3]='b',不相等。
现在需要回退:j = next[2] = 1(因为next[2]是位置2的最长前后缀长度,即子串"aba"的前后缀长度1,所以新的候选前缀是s[0]="a")。
然后比较s[5]='c'和s[1]='b',还是不相等。
再回退:j = next[0] = 0。
此时j=0,退出while。比较s[5]='c'和s[0]='a',不相等,j保持0。next[5]=0。
子串"ababac"没有相同前后缀。 -
i=6, j=0:比较
s[6]='a'和s[0]='a',相等,j++变为1,next[6]=1。
子串"ababaca"的前缀"a"和后缀"a"相同,长度1。
通过这个例子,你可以看到每次不匹配时,j都会回退到之前某个位置的next值,而不是直接回退到0。这样做保证了我们总是尝试最长的可能前缀,从而高效地计算出正确的next数组。
5. 为什么这样是正确的
简单来说,就是利用了传递性:
- 已知
s[0..j-1] == s[i-j..i-1]。 - 如果
s[0..j-1]有长度为k的相同前后缀,那么s[0..k-1] == s[j-k..j-1]。 - 结合上面两个等式,就能推出
s[0..k-1] == s[i-k..i-1]。
所以k也是一个可行的候选长度。我们通过不断取next[j-1],得到所有可能的候选长度(从大到小),直到找到第一个能让s[k] == s[i]的k,那么这个k+1就是我们要找的next[i]。
因为我们是按长度从大到小尝试的,所以第一次找到的就是最长的。
6. 总结
j表示当前已经匹配的前缀长度,也代表下一个要比较的字符在模式串中的位置。- 当
s[i] != s[j]时,我们利用next[j-1]快速跳到下一个可能匹配的前缀长度,而不是笨拙地只减少1。 - 这种跳跃保证每个字符最多比较常数次,使得整个算法复杂度为O(n)。
希望这个解释能帮你理解next数组计算的本质!