字符串匹配是日常开发中最常见的操作------搜索关键词、查找替换、文本比对。暴力匹配虽然简单但效率低,KMP 算法是考研和面试的常考题,也是理解"空间换时间"思想的好例子。
一、串的基本概念
串(String) 是由零个或多个字符组成的有限序列。
S = "abc123"
↑ ↑
串名 串值
术语:
- 子串:串中任意连续字符组成的子序列。如 "abc" 是 "abc123" 的子串
- 主串:包含子串的串
- 位置:字符在串中的序号(通常从 1 开始)
二、暴力匹配(Brute-Force)
1. 算法思想
主串 S: "ababcabcacbab"
模式串 T: "abcac"
从 S[1] 开始与 T[1] 比较:
第一趟:a b a b c ...
a b c a c ← S[3]=a ≠ T[3]=c,匹配失败
↑
第二趟:a b a b c ...
a b c a c ← S[2]=b ≠ T[1]=a,失败
第三趟:a b a b c a b c a c b a b
a b c a c ← 匹配成功!
↑
2. 代码实现
java
public class BruteForce {
/**
* 暴力匹配
* @param s 主串
* @param t 模式串
* @return 匹配到的起始位置,-1 表示未找到
*/
public static int indexOf(String s, String t) {
int n = s.length();
int m = t.length();
// i 为主串指针,j 为模式串指针
for (int i = 0; i <= n - m; i++) {
int j;
for (j = 0; j < m; j++) {
if (s.charAt(i + j) != t.charAt(j)) {
break; // 字符不相等,跳出内循环
}
}
if (j == m) {
return i; // 完全匹配,返回起始位置
}
}
return -1;
}
}
3. 时间复杂度
最好情况:每次第一个字符就不匹配 → O(n + m)
最坏情况:每次匹配到最后一个字符才失败 → O(n × m)
例子:S = "aaaaaaaaaaaab", T = "aaab"
每一趟都要比到最后才发现不匹配 → 效率极低
暴力匹配的问题: 匹配失败后主串回溯到 i+1,模式串回到 0,之前比过的信息全部丢弃。
三、KMP 算法
1. 核心思想
KMP 算法(Knuth-Morris-Pratt)的核心是:匹配失败时,主串指针不回溯,模式串根据 next 数组向右滑动到合适位置继续匹配。
主串: "ababcabcacbab"
模式串: "abcac"
当匹配到第 3 个字符失败时:
a b a b c ...
a b c a c
↑ ↑ ← S[3]≠T[3]
i=2 ← 且 S[0:2]="ab" = T[0:2]="ab"
暴力做法:i 回退到 1,j 回退到 0 → 重新比
KMP 做法:i 不动(停在 2),j 跳到 1 → 因为 T[0:1]="a" = T[1:2]="b"? 不相等,所以 j=0
关键:模式串 T 中已经匹配的部分 "ab" 有没有重复的前后缀?
"ab" 的前缀 "a" ≠ 后缀 "b" → 没有公共前后缀 → j 回到 0
2. next 数组
nextj 的含义: 当模式串 Tj 与主串匹配失败时,j 应该回退到的位置。
计算规则:
next[0] = -1(模式串第一个字符就失败)
next[1] = 0(第二个字符失败,模式串回到开头)
对于 j > 1:
next[j] = T[0:j-1] 中最长相等前后缀的长度
前后缀概念:
字符串 "abcab"
前缀:{"a", "ab", "abc", "abca"}
后缀:{"b", "ab", "cab", "bcab"}
最长相等前后缀 = "ab" → 长度 = 2
3. 手算 next 数组
模式串 T = "ababa"
j=0: next[0] = -1
j=1: T[0:0]="" → 没有子串 → next[1] = 0
j=2: T[0:1]="ab"
前缀: {"a"} 后缀: {"b"} → 无相等 → next[2] = 0
j=3: T[0:2]="aba"
前缀: {"a", "ab"} 后缀: {"a", "ba"}
相等: "a" → 长度1 → next[3] = 1
j=4: T[0:3]="abab"
前缀: {"a", "ab", "aba"} 后缀: {"b", "ab", "bab"}
相等: "ab" → 长度2 → next[4] = 2
结果:next = [-1, 0, 0, 1, 2]
4. KMP 匹配过程
用上面计算好的 next 数组匹配:
S = "abcabababa"
T = "ababa"
next = [-1, 0, 0, 1, 2]
第一趟:a b c a b a b a b c
a b a b a
↑
i=2: S[2]=c ≠ T[2]=a,失败
j=2 → next[2]=0 → j 跳到 0,i 不变
第二趟:a b c a b a b a b c
a b a b a
↑
i=2: S[2]=c ≠ T[0]=a,失败
j=0 → next[0]=-1 → i 后移一位,j=0
第三趟:a b c a b a b a b c
a b a b a
↑↑↑
S[3]=a=T[0], S[4]=b=T[1], S[5]=a=T[2] → 匹配继续...
... 最终匹配成功
5. 代码实现
java
public class KMP {
/**
* 计算 next 数组
*/
public static int[] getNext(String pattern) {
int m = pattern.length();
int[] next = new int[m];
next[0] = -1;
int i = 0; // 当前计算的 next 位置
int j = -1; // 最长相等前后缀长度
while (i < m - 1) {
if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
i++;
j++;
next[i] = j;
} else {
j = next[j]; // 不相等,回退
}
}
return next;
}
/**
* KMP 匹配
*/
public static int indexOf(String text, String pattern) {
int n = text.length();
int m = pattern.length();
if (m == 0) return 0;
int[] next = getNext(pattern);
int i = 0; // 主串指针
int j = 0; // 模式串指针
while (i < n && j < m) {
if (j == -1 || text.charAt(i) == pattern.charAt(j)) {
i++;
j++;
} else {
j = next[j]; // 主串不回溯,只动 j
}
}
if (j == m) {
return i - j; // 匹配成功
}
return -1;
}
}
6. 时间复杂度
计算 next 数组:O(m)
KMP 匹配过程:O(n)
总时间复杂度:O(n + m)
对比:
暴力:O(n × m)
KMP:O(n + m)
当 n=1000000, m=10000 时:
暴力:100 亿次比较
KMP:101 万次比较 → 快 10000 倍
四、KMP 的优化(nextval 数组)
KMP 有一个改进版,使用 nextval 数组进一步减少不必要的比较:
java
public static int[] getNextVal(String pattern) {
int m = pattern.length();
int[] nextval = new int[m];
nextval[0] = -1;
int i = 0;
int j = -1;
while (i < m - 1) {
if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
i++;
j++;
// 改进:如果 pattern[i] == pattern[j]
// 那么即使跳到 j 也会匹配失败,所以继续向前跳
if (pattern.charAt(i) != pattern.charAt(j)) {
nextval[i] = j;
} else {
nextval[i] = nextval[j];
}
} else {
j = nextval[j];
}
}
return nextval;
}
五、408 考研常见考题
题1:手算 next 数组
求模式串 "abaabcac" 的 next 数组
答案:[-1, 0, 0, 1, 1, 2, 2, 3]
过程:
j=0: next[0] = -1
j=1: next[1] = 0
j=2: "ab" → 无相等 → next[2]=0
j=3: "aba" → "a"=1 → next[3]=1
j=4: "abaa" → "a"=1 → next[4]=1
j=5: "abaab" → "ab"=2 → next[5]=2
j=6: "abaabc" → 无相等 → next[6]=0→next[6]=2?
j=7: "abaabca" → "a"=1→next[7]=1→...
题2:next 与 nextval 的区别
模式串 "aaaaaab"
next 数组: [-1, 0, 1, 2, 3, 4, 5]
nextval 数组: [-1, -1, -1, -1, -1, -1, 5]
优化效果:当遇到 "aaaaaab" 时,
遇到 'b' 之前的所有字符都是 'a',
如果 S[i] ≠ 'a',那么跳转到哪里都一样,
nextval 直接跳到第一个字符
六、KMP 算法的应用
java
// 1. 文本搜索(Ctrl+F)
int pos = KMP.indexOf(document, keyword);
// 2. 查找所有出现位置
List<Integer> findAll(String text, String pattern) {
List<Integer> positions = new ArrayList<>();
int pos = 0;
while ((pos = KMP.indexOf(text.substring(pos), pattern)) != -1) {
positions.add(pos);
pos++;
}
return positions;
}
// 3. 字符串替换
String replace(String text, String oldStr, String newStr) {
StringBuilder sb = new StringBuilder();
int last = 0;
int pos = KMP.indexOf(text, oldStr);
while (pos != -1) {
sb.append(text, last, pos);
sb.append(newStr);
last = pos + oldStr.length();
pos = KMP.indexOf(text.substring(last), oldStr);
if (pos != -1) pos += last;
}
sb.append(text.substring(last));
return sb.toString();
}
七、知识点对比
暴力匹配:主串回溯,简单但慢
KMP:主串不回溯,利用 next 数组快速跳转
next 数组:记录最长相等前后缀长度
nextval 数组:next 的改进版,进一步优化
暴力 O(n×m) → 理解思想,面试基础
KMP O(n+m) → 考研重点,要能手算 next 数组
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。