#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. 代码怎么工作的

代码中用两个指针ij

  • 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-1next值,也就是说我们知道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数组计算的本质!