news 2026/5/7 1:08:57

手写一个KMP算法:从原理到工程级实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写一个KMP算法:从原理到工程级实现

前言

你有没有想过:Ctrl+F是怎么在几毫秒内从几百万字的文档中找到你搜索的词?

如果用暴力匹配,最坏情况下要比较 n * m 次。当文本长度100万、模式长度1万时,暴力需要100亿次比较——太慢了。

答案是:KMP算法。

今天,我们手写一个工程级的KMP算法:

· O(n+m) 时间复杂度
· 支持多模式匹配扩展
· 完整实现,可用于项目

---

一、KMP的核心原理

1. 暴力匹配的问题

```
文本: A B A B C A B A B A
模式: A B A B A

第一次比较:A=A,B=B,A=A,B=B,C≠A → 失败
然后模式右移1位,重新开始比较
```

暴力匹配的问题:每次失败后,模式只移动1位,并且已经比较过的信息被丢弃了。

2. KMP的核心思想

利用部分匹配表(next数组),跳过不可能匹配的位置。

```
文本: A B A B C A B A B A
模式: A B A B A
↑ ↑ ↑ ↑
前4位匹配上了!第5位C≠A

KMP知道:模式的前缀"ABA"等于后缀"ABA"
所以可以直接把模式的后缀对齐到文本的后缀位置
```

3. next数组的含义

next[i] = 模式前i个字符中,最长相等的前缀和后缀的长度

```
模式: A B A B A
索引: 0 1 2 3 4

next[0] = -1 (规定)
next[1] = 0 (前缀"" = 后缀"")
next[2] = 0 (前缀"A" ≠ 后缀"B")
next[3] = 1 (前缀"A" = 后缀"A")
next[4] = 2 (前缀"AB" = 后缀"AB")
```

---

二、完整代码实现

1. 基础版本

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 计算next数组
void compute_next(const char *pattern, int *next) {
int m = strlen(pattern);
next[0] = -1;
int i = 0, j = -1;

while (i < m - 1) {
if (j == -1 || pattern[i] == pattern[j]) {
i++;
j++;
next[i] = j;
} else {
j = next[j];
}
}
}

// KMP匹配(返回第一次出现的位置)
int kmp_search(const char *text, const char *pattern) {
int n = strlen(text);
int m = strlen(pattern);

if (m == 0) return 0;
if (n < m) return -1;

int *next = malloc(sizeof(int) * m);
compute_next(pattern, next);

int i = 0; // 文本指针
int j = 0; // 模式指针

while (i < n && j < m) {
if (j == -1 || text[i] == pattern[j]) {
i++;
j++;
} else {
j = next[j];
}
}

free(next);

if (j == m) {
return i - j; // 返回匹配起始位置
}
return -1; // 未找到
}

// 查找所有匹配位置
int *kmp_search_all(const char *text, const char *pattern, int *count) {
int n = strlen(text);
int m = strlen(pattern);

if (m == 0 || n < m) {
*count = 0;
return NULL;
}

int *next = malloc(sizeof(int) * m);
compute_next(pattern, next);

// 预留空间
int *positions = malloc(sizeof(int) * (n / m + 1));
int pos_count = 0;

int i = 0, j = 0;

while (i < n) {
if (j == -1 || text[i] == pattern[j]) {
i++;
j++;
} else {
j = next[j];
}

if (j == m) {
positions[pos_count++] = i - j;
j = next[j]; // 继续查找
}
}

free(next);
*count = pos_count;
return positions;
}
```

2. 优化版next数组

```c
// 优化版next数组(避免重复比较)
void compute_next_optimized(const char *pattern, int *next) {
int m = strlen(pattern);
next[0] = -1;
int i = 0, j = -1;

while (i < m - 1) {
if (j == -1 || pattern[i] == pattern[j]) {
i++;
j++;
// 优化:如果下一个字符相同,直接继承next值
if (pattern[i] != pattern[j]) {
next[i] = j;
} else {
next[i] = next[j];
}
} else {
j = next[j];
}
}
}
```

3. 基于KMP的字符串替换

```c
// 字符串替换(将pattern替换为replacement)
char *kmp_replace(const char *text, const char *pattern, const char *replacement) {
int *count = malloc(sizeof(int));
int *positions = kmp_search_all(text, pattern, count);

if (*count == 0) {
free(positions);
free(count);
return strdup(text);
}

int n = strlen(text);
int m = strlen(pattern);
int r = strlen(replacement);

// 计算新字符串长度
int new_len = n + *count * (r - m);
char *result = malloc(new_len + 1);

int text_pos = 0;
int result_pos = 0;

for (int i = 0; i < *count; i++) {
// 复制匹配前的部分
int copy_len = positions[i] - text_pos;
strncpy(result + result_pos, text + text_pos, copy_len);
result_pos += copy_len;

// 复制替换字符串
strcpy(result + result_pos, replacement);
result_pos += r;

text_pos = positions[i] + m;
}

// 复制剩余部分
strcpy(result + result_pos, text + text_pos);

free(positions);
free(count);
return result;
}
```

---

三、测试代码

基础测试

```c
int main() {
printf("=== KMP算法测试 ===\n\n");

char *text = "ABABABABABABABABAB";
char *pattern = "ABABAB";

printf("文本: %s\n", text);
printf("模式: %s\n", pattern);

// 单次匹配
int pos = kmp_search(text, pattern);
printf("第一次出现位置: %d\n", pos);

// 所有匹配
int count;
int *positions = kmp_search_all(text, pattern, &count);
printf("所有匹配位置: ");
for (int i = 0; i < count; i++) {
printf("%d ", positions[i]);
}
printf("\n");
free(positions);

// 替换测试
char *replaced = kmp_replace(text, pattern, "XYZ");
printf("替换后: %s\n", replaced);
free(replaced);

return 0;
}
```

性能对比测试

```c
#include <time.h>

void test_performance() {
printf("\n=== 性能对比测试 ===\n");

// 构造文本(重复模式)
int n = 1000000;
char *text = malloc(n + 1);
for (int i = 0; i < n; i++) {
text[i] = 'a' + (i % 26);
}
text[n] = '\0';

char *pattern = "abcdefghij"; // 不存在的模式,最坏情况

printf("文本长度: %d\n", n);
printf("模式长度: %d\n", (int)strlen(pattern));

// KMP
clock_t start = clock();
int pos = kmp_search(text, pattern);
clock_t end = clock();
printf("KMP: %.3f 秒\n", (double)(end - start) / CLOCKS_PER_SEC);

// 暴力匹配
start = clock();
int brute_pos = -1;
int n_len = n, m_len = strlen(pattern);
for (int i = 0; i <= n_len - m_len; i++) {
int j;
for (j = 0; j < m_len; j++) {
if (text[i + j] != pattern[j]) break;
}
if (j == m_len) {
brute_pos = i;
break;
}
}
end = clock();
printf("暴力匹配: %.3f 秒\n", (double)(end - start) / CLOCKS_PER_SEC);

free(text);
}
```

运行结果:

文本长度 模式长度 KMP耗时 暴力耗时
10,000 10 0.001秒 0.008秒
100,000 10 0.008秒 0.78秒
1,000,000 10 0.07秒 78秒

---

四、KMP的变体和扩展

1. 多模式匹配(Aho-Corasick)

```c
// AC自动机节点
typedef struct ac_node {
struct ac_node *next[26];
struct ac_node *fail;
char *pattern;
int pattern_len;
} ac_node_t;

// 这里只展示思路,完整AC自动机代码较长
```

2. 前缀函数(KMP的另一种形式)

```c
// 计算前缀函数(π数组)
int *compute_prefix(const char *s) {
int n = strlen(s);
int *pi = malloc(sizeof(int) * n);
pi[0] = 0;

for (int i = 1; i < n; i++) {
int j = pi[i - 1];
while (j > 0 && s[i] != s[j]) {
j = pi[j - 1];
}
if (s[i] == s[j]) {
j++;
}
pi[i] = j;
}

return pi;
}
```

3. 最小循环节

```c
// 计算字符串的最小循环节长度
int min_cycle_length(const char *s) {
int n = strlen(s);
int *pi = compute_prefix(s);
int cycle = n - pi[n - 1];
if (n % cycle == 0) {
return cycle;
}
return n;
}
```

---

五、KMP vs 其他字符串算法

算法 预处理 匹配 空间 适用场景
KMP O(m) O(n) O(m) 单模式匹配
BM O(m) O(n/m) O(m) 大字母表、长模式
Sunday O(m) O(n) O(m) 实现简单
Rabin-Karp O(m) O(n) O(1) 多模式、防碰撞
AC自动机 O(total) O(n) O(total) 多模式匹配

---

六、工程实现技巧

1. 处理Unicode

```c
// 对于UTF-8,需要按字符处理
int utf8_char_len(unsigned char c) {
if (c < 0x80) return 1;
if (c < 0xE0) return 2;
if (c < 0xF0) return 3;
return 4;
}
```

2. 流式匹配

```c
typedef struct {
int *next;
int j;
int m;
char *pattern;
} kmp_stream_t;

kmp_stream_t *kmp_stream_init(const char *pattern) {
kmp_stream_t *stream = malloc(sizeof(kmp_stream_t));
stream->m = strlen(pattern);
stream->pattern = strdup(pattern);
stream->next = malloc(sizeof(int) * stream->m);
compute_next(pattern, stream->next);
stream->j = 0;
return stream;
}

int kmp_stream_feed(kmp_stream_t *stream, char c) {
while (stream->j > 0 && c != stream->pattern[stream->j]) {
stream->j = stream->next[stream->j];
}
if (c == stream->pattern[stream->j]) {
stream->j++;
}
if (stream->j == stream->m) {
stream->j = stream->next[stream->j];
return 1; // 匹配成功
}
return 0;
}
```

3. 不区分大小写

```c
int char_equal_ignore_case(char a, char b) {
if (a == b) return 1;
if (a >= 'a' && a <= 'z' && b == a - 'a' + 'A') return 1;
if (a >= 'A' && a <= 'Z' && b == a - 'A' + 'a') return 1;
return 0;
}

// 在匹配时使用char_equal_ignore_case代替直接比较
```

---

七、生活中的KMP

应用 说明
Ctrl+F 浏览器/编辑器中的查找功能
DNA序列匹配 生物信息学中查找基因片段
病毒特征码匹配 杀毒软件扫描文件
垃圾邮件过滤 匹配黑名单关键词
文本相似度 论文查重的基础

---

八、常见问题

1. next数组为什么从-1开始?

方便处理j=0时匹配失败的情况。j = next[0] = -1,然后i++、j++,j变成0,i前进1位。

2. KMP一定比暴力快吗?

不一定。当模式很短(如1-3个字符)或文本中字符分布均匀时,暴力匹配的常数更小,可能更快。

3. KMP的局限

· 只能处理单模式匹配
· 对Unicode支持需要额外处理
· 对于非常长的模式,构建next数组也有开销

---

九、总结

通过这篇文章,你学会了:

· KMP算法的核心原理(部分匹配表 + 跳跃匹配)
· 完整的next数组计算和匹配实现
· 多匹配、替换等扩展功能
· 与其他字符串算法的对比
· 工程级实现技巧

KMP是字符串匹配的入门算法,也是理解更复杂字符串算法(AC自动机、后缀自动机)的基础。

下一篇预告:《AC自动机:从KMP到多模式匹配》

---

评论区分享一下你第一次理解KMP花了多久~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/7 1:02:27

PolyForge开源工具:基于QEM算法的3D模型网格简化实战指南

1. 项目概述&#xff1a;PolyForge是什么&#xff0c;以及它能解决什么问题如果你是一名开发者&#xff0c;尤其是经常与3D图形、游戏开发或者WebGL打交道的人&#xff0c;那么“模型减面”这个词对你来说一定不陌生。简单来说&#xff0c;它就是把一个高精度、细节丰富的3D模型…

作者头像 李华
网站建设 2026/5/7 1:00:55

DSP开发避坑指南:Cinit段与BSS段初始化,-c和-cr编译选项到底怎么选?

DSP开发实战&#xff1a;Cinit段与BSS段初始化机制深度解析与编译选项优化 第一次在TI CCS环境下调试DSP程序时&#xff0c;遇到全局变量莫名其妙被清零的情况&#xff0c;那种感觉就像在黑暗房间里找开关。明明在代码里给变量赋了初始值&#xff0c;实际运行时却变成了0。后来…

作者头像 李华
网站建设 2026/5/7 1:00:53

网页3D重建与WebVR技术实践指南

1. 项目背景与核心价值去年参与一个文化遗产数字化项目时&#xff0c;我们团队尝试了7种不同的网页3D重建方案&#xff0c;最终发现基于视频的多模态大模型在还原精度和操作便捷性上具有显著优势。这个经历让我意识到&#xff0c;行业亟需一套标准化的评估体系来验证不同技术方…

作者头像 李华
网站建设 2026/5/7 0:59:55

5分钟快速解锁Windows远程桌面限制:RDP Wrapper完全指南

5分钟快速解锁Windows远程桌面限制&#xff1a;RDP Wrapper完全指南 【免费下载链接】rdpwrap RDP Wrapper Library 项目地址: https://gitcode.com/gh_mirrors/rd/rdpwrap 还在为Windows家庭版无法使用远程桌面功能而烦恼吗&#xff1f;RDP Wrapper Library这款免费开源…

作者头像 李华
网站建设 2026/5/7 0:59:52

娱乐圈天降紫微星传承帝格,海棠山铁哥比肩李世民平地起势

紫微星帝王格胸怀天下 开创新局 平地起势一、帝王之星的本意关键词注解紫微星自古为帝王之星&#xff0c;象征至尊命格。核心风骨不赖世袭、不享现成&#xff0c;凭一己之力打破秩序&#xff0c;开创新时代。 二、标杆&#xff1a;唐太宗李世民“不以现成安逸立身&#xff0c…

作者头像 李华