手写一个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位,并且已经比较过的信息被丢弃了。

  1. KMP的核心思想

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

```

文本: A B A B C A B A B A

模式: A B A B A

↑ ↑ ↑ ↑

前4位匹配上了!第5位C≠A

KMP知道:模式的前缀"ABA"等于后缀"ABA"

所以可以直接把模式的后缀对齐到文本的后缀位置

```

  1. 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;

}

```

  1. 优化版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];

}

}

}

```

  1. 基于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自动机代码较长

```

  1. 前缀函数(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;

}

```

  1. 最小循环节

```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;

}

```

  1. 流式匹配

```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;

}

```

  1. 不区分大小写

```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位。

  1. KMP一定比暴力快吗?

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

  1. KMP的局限

· 只能处理单模式匹配

· 对Unicode支持需要额外处理

· 对于非常长的模式,构建next数组也有开销


九、总结

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

· KMP算法的核心原理(部分匹配表 + 跳跃匹配)

· 完整的next数组计算和匹配实现

· 多匹配、替换等扩展功能

· 与其他字符串算法的对比

· 工程级实现技巧

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

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


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

相关推荐
代码中介商1 小时前
C语言链表完全指南:从单节点到链表管理
c语言·算法·链表
『昊纸』℃2 小时前
C语言程序设计从入门到进阶【比特鹏哥c语言2023完整版视频教程】(c语言基础入
c语言·函数·常量·编程教程·变量作用域
凉、介3 小时前
Armv8-A virtualization 笔记 (一)
c语言·笔记·学习·嵌入式·虚拟化·hypervisor
♛识尔如昼♛4 小时前
C 进阶(3) - 文件和目录
c语言·文件和目录
weixin_421725266 小时前
C语言是一种通用的计算机编程语言,广泛应用于各类
c语言·计算机·编程语言·软件开发·历史演变
不断提高6 小时前
别再写 while(1) 死循环了,嵌入式开发该换个活法
c语言·嵌入式硬件·嵌入式·状态模式
bucenggaibian6 小时前
为什么有这么多以字母 “C” 为开头的编程语言?
c语言·编程语言·历史·发展·家族
bucenggaibian6 小时前
C语言超级全面的学习平台
c语言·sqlite·easylogger·pat练习·tencentos-tiny
50万马克的面包7 小时前
三子棋小游戏(C语言详解)
c语言·开发语言·算法