字符串匹配是每个程序员最早接触的算法:在一个文本里找一段模式串。

暴力做法O(mn),面试官点点头,然后问:“还能优化吗?”

这就是KMP的登场时刻。它的全部洞察浓缩成一句话:失配时主串指针绝不回退,只移动模式串。

为什么敢这么干?

已匹配的那段文本里,存在可以被复用的“最长相等真前后缀”。

今天把三件事彻底讲透:

  1. 什么是“最长相等真前后缀”
  2. next数组怎么构造(本质是模式串自己跟自己做一次KMP)
  3. 失配时j = next[j]到底在说什么

📦 题目速览 LeetCode 28(30秒读懂)

给定haystack和needle,返回needle在haystack中第一次出现的下标,不存在返回-1。

示例:

  • haystack = "sadbutsad", needle = "sad" → 0
  • haystack = "leetcode", needle = "leeto" → -1
  • haystack = "aabaabaaf", needle = "aabaaf" → 3(今天图解主角)

约束: 长度 ≤ 1e4,仅小写字母。
提示: 可以调库过题,但面试考的就是自己实现。


🧠 核心思路:用“最长相等真前后缀”量化失配时能滑多远

暴力为什么慢?

暴力匹配:拿模式串对准主串,逐字符比对;一旦失配,主串指针i回退到i+1,模式串j归零,从头再比。

病根只有一条:i回退了。 主串里已经比对过的字符,被白白重新比对。

KMP 的洞察

模式串已经匹配的前j个字符的信息,我们都拿到了——为什么不复用?

核心概念:最长相等真前后缀(PMT)

对模式串的某个前缀p[0..k],把所有“既是它的前缀、又是它的后缀”的子串叫相等前后缀;其中长度最长的那个,长度记为PMT值。“真”字意味着这个子串不能等于它自己。

以p = "aabaaf"举例:

前缀最长相等真前后缀长度 PMT
"a"—0
"aa""a"1
"aab"—0
"aaba""a"1
"aabaa""aa"2
"aabaaf"—0

得到 PMT = [0, 1, 0, 1, 2, 0]。

这个表为什么有用?

假设已匹配前5个字符"aabaa",第6个失配:

主串 s:   ... a a b a a | X ...     ← 已知这5个字符 == p的前5个
模式 p:       a a b a a | f
                          ↑ 失配

暴力会丢掉一切重来。但我们知道:"aabaa"的最长相等真前后缀是"aa"(长度2)。这意味着:

  • 它的前缀 "aa" == 它的后缀 "aa"
  • 后缀"aa"已经在主串里被确认过了
  • 所以可以直接把模式串右移,让前缀"aa"对准主串那两个字符,j跳到2继续比——中间字符不用重比!

一句话:失配时模式串能滑多远,取决于已匹配部分的最长相等真前后缀有多长。

next数组:把PMT右移一位

工程上把 PMT 整体右移一位,next[0] = -1:

p      :  a   a   b   a   a   f
PMT    : [0,  1,  0,  1,  2,  0]
next   : [-1, 0,  1,  0,  1,  2]   ← next[i] = PMT[i-1]

这样一来:

  • 第j位失配时,直接j = next[j]——无需下标特判
  • next[j] = -1是哨兵信号:“没有任何前缀可复用,主串指针i也往后走一格”

next数组怎么构造?——模式串自己和自己做一次KMP

这是最惊艳的部分:构造next和匹配主串共用同一份控制流。

用两个指针i(当前要算next的位置)和j(当前已匹配的前缀长度),让p的后缀去“匹配”p自己的前缀:

  • 若p[i] == p[j]:前缀延长,i++、j++,记next[i] = j
  • 若失配或j == -1:j按next[j]回退,i不动;回退到-1则双方一起前进,记next[i] = 0

这既是动态规划(用已求出的next[0..i-1]推next[i]),也是“自匹配”。


🖼️ 图解算法(手把手走一遍)

1)构造next数组(p = "aabaaf")

轮次ij比较动作写入
初始0-1j == -1双方前进 → i=1, j=0next[1] = 0
110'a' vs 'a' ✅双方前进 → i=2, j=1next[2] = 1
221'b' vs 'a' ❌j = next[1] = 0—
320'b' vs 'a' ❌j = next[0] = -1—
42-1j == -1双方前进 → i=3, j=0next[3] = 0
530'a' vs 'a' ✅双方前进 → i=4, j=1next[4] = 1
641'a' vs 'a' ✅双方前进 → i=5, j=2next[5] = 2

结果 next = [-1, 0, 1, 0, 1, 2]。注意:i从不回退,只有j在按next跳转。

2)用它去匹配(s = "aabaabaaf", p = "aabaaf")

下标:     0  1  2  3  4  5  6  7  8
主串 s:   a  a  b  a  a  b  a  a  f
模式 p:   a  a  b  a  a  f
步骤ij比较结果说明
10→50→5连续5次相同双前进已匹配 "aabaa"
255s[5]='b' vs p[5]='f'❌ 失配j = next[5] = 2,i纹丝不动!
352s[5]='b' vs p[2]='b'✅ 双前进复用已验证的"aa"
46→83→5a、a、f三次相同双前进全部匹配
596—返回i - m = 9 - 6 = **3**✅

关键观察:整张表里 i从0单调走到9,一次都没有回退。第2步是高光时刻——失配时不看主串历史,只查next[5] = 2。

对照暴力:它得把i从5退回1,重新比对,白干4次。


💻 代码实现(Python + Java)

Python版

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n, m = len(haystack), len(needle)
        if m == 0:
            return 0
        nxt = self._build_next(needle)

        i = j = 0                                 # i主串指针(永不回退)
        while i < n and j < m:
            if j == -1 or haystack[i] == needle[j]:
                i += 1
                j += 1                            # 匹配(或j是哨兵-1)→ 双双前进
            else:
                j = nxt[j]                        # ★ 失配:只回退 j,i一动不动
        return i - m if j == m else -1

    @staticmethod
    def _build_next(p: str) -> list:
        """构造next数组:本质是模式串的前缀与后缀自己跟自己做一次匹配"""
        m = len(p)
        nxt = [-1] * m                            # next[0] = -1
        i, j = 0, -1
        while i < m - 1:
            if j == -1 or p[i] == p[j]:
                i += 1
                j += 1
                nxt[i] = j                        # 前缀成功延长一位
            else:
                j = nxt[j]                        # ★ 失配:回退到更短的可复用前缀
        return nxt

Java 版

class Solution {
    public int strStr(String haystack, String needle) {
        int n = haystack.length(), m = needle.length();
        if (m == 0) return 0;
        int[] next = buildNext(needle);

        int i = 0, j = 0;                         // i主串指针(永不回退)
        while (i < n && j < m) {
            if (j == -1 || haystack.charAt(i) == needle.charAt(j)) {
                i++;
                j++;
            } else {
                j = next[j];                      // ★ 只移动模式串
            }
        }
        return j == m ? i - m : -1;
    }

    private int[] buildNext(String p) {
        int m = p.length();
        int[] next = new int[m];
        next[0] = -1;
        int i = 0, j = -1;
        while (i < m - 1) {
            if (j == -1 || p.charAt(i) == p.charAt(j)) {
                i++;
                j++;
                next[i] = j;
            } else {
                j = next[j];                      // ★ 递推:回退找次长前缀
            }
        }
        return next;
    }
}

⚠️ 防坑提醒(必看):

  • while (i < m - 1)而不是 < m——next[0]已定为 -1,写成 < m会下标越界。
  • j == -1哨兵分支绝不能漏,它负责“彻底没得复用时让i前进”。
  • 匹配成功返回i - m(循环里i已多走一格),不是i - j。
  • 两段代码结构几乎一模一样——记住“自匹配”这个比喻,两份代码一起就都记住了。

⏱️ 复杂度分析(面试必问)

阶段时间说明
构造 nextO(m)i单调增长,j回退总量 ≤ 前进总量
匹配O(n)i从不回退,j回退受限于前进次数
总计O(n + m)相比暴力O(n·m) 是降维打击
空间O(m)next数组本身,主串无需额外空间

常见误解:有人认为KMP是O(n·m),因为j可能回退很多次。不对——j的回退严格受限于它之前的前进次数,而前进总和 ≤ n,所以回退总和也 ≤ n。这是均摊分析的经典小练习,面试讲出来很加分。


🚀 举一反三:5道高频变体题

题目变化点思路要点
LC.459 重复的子字符串判断能否由子串重复构成求PMT,答案 = pmt[n-1] > 0 && n % (n - pmt[n-1]) == 0
LC.686 重复叠加字符串匹配A重复几次能包含BKMP匹配时允许i循环复用A
LC.214 最短回文串开头补最少字符变回文构造s + "#" + reverse(s)求PMT
LC.1392 最长快乐前缀求最长相等真前后缀本身直接就是PMT的定义题
LC.28(今天)找首次出现的下标next数组 + 匹配

💬 面试追问模拟(提前准备,惊艳全场)

Q1:next数组为什么要右移一位?PMT和next有什么区别?

PMT[i]定义为“p[0…i]的最长相等真前后缀长度”,用它时失配得写j = pmt[j-1],还得单独判断j == 0。右移一位并令next[0] = -1 之后,next[j]的语义变成“在第j位失配时,j应该跳到哪里”,循环里统一写成j = next[j]。这是一次纯粹为了代码优雅而做的下标平移,信息量完全等价。

Q2:怎么求next数组?能口述递推过程吗?

设已经算出next[0..i-1],当前比较p[i]与p[j]。
若相等,说明最长前后缀延长一位:next[i] = ++j。
若不等,不是归零重来,而是令j = next[j]去找“次长的候选”继续试——因为next[j]已经告诉了我们“在j这个位置失配该退到哪”。退到-1说明没有可用的,next[i] = 0。
这就是DP的“子问题复用”。

Q3:BM / Sunday实际更快,为什么面试还是考KMP?

事实确实如此:Boyer-Moore和Sunday在真实文本上通常比KMP快数倍,各种标准库都用它们的变体或SIMD版本。但 KMP有两个不可替代的价值:
① 它是最坏情况严格O(n+m) 的算法;
② next数组所体现的“预处理自身 + 有限状态自动机 + 均摊分析”是一整套方法论,能考察候选人对DP和不变式的理解。
面试官考KMP,考的是思维,不是性能。

Q4:KMP在工程里真的有用吗?

有。典型场景:流式/单趟扫描(主串指针不能回退、数据只来一遍)——网络包特征码扫描、日志流实时过滤、编译器词法分析。
KMP的“主串指针永不回退”天然适配。此外,基因序列比对、IDE增量搜索、以及LC.459 / LC.214这类“利用前后缀性质”的题,底层都是 PMT。


🧩 实战小技巧(刷题党必备)

  • 口诀:失配别回退主串,next表里找答案;最长相等真前后缀,模式自匹配一遍。
  • 模板:KMP = 构造next(自匹配)+ 匹配(i不回退)。
  • 防坑:while (i < m - 1);j == -1哨兵别漏;返回i - m。

📈 实际应用场景(不止是刷题)

  • 编辑器查找:大文件单趟扫描
  • WAF / 杀毒软件:特征串匹配(多条规则升级为AC自动机)
  • 网络协议DPI:深度包检测
  • 生物信息学:DNA序列比对
  • RocksDB / LevelDB:某些前缀相关优化

🎁 今日思考题

不看代码,用“最长相等真前后缀”的定义手推p = "ababaca"的next数组。
提示:答案应该是[-1, 0, 0, 1, 2, 3, 0]。

另外思考:为什么构造next时p[i] == p[j]失配后可以令j = next[j]而不是归零?

Logo

一站式 AI 云服务平台

更多推荐