数据结构KMP算法详解:C语言实现

算法概述

KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法,由D.E.Knuth、J.H.Morris和V.R.Pratt于1977年联合发表。该算法通过预处理模式串,构建next数组,在匹配失败时利用已有信息跳过不必要的比较,将时间复杂度从暴力匹配的O(m*n)优化到O(m+n)。

算法设计思路

设计动机与核心思想

问题分析:

传统暴力匹配算法在每次匹配失败时,主串指针都要回溯到上一次匹配位置的下一个字符,模式串指针回到开头,导致大量重复比较。

核心洞察:

当匹配失败时,我们其实已经获得了一些有用的信息------已经成功匹配的前缀部分。KMP算法的核心思路就是利用这些信息,避免主串指针的回溯,只移动模式串指针。

设计思路演变:

  1. 观察现象:在匹配过程中,部分匹配的前缀可能包含重复的模式

  2. 关键发现:已匹配部分的前缀和后缀可能存在公共部分

  3. 解决方案:构建next数组记录这些信息,指导匹配失败时的回溯位置

  4. 优化目标:主串指针不回溯,只调整模式串指针位置

算法核心原理

部分匹配表(Next数组)

Next数组是KMP算法的核心,它记录了模式串中"前缀"和"后缀"的最长公共元素长度。当匹配失败时,Next数组告诉我们下一步应该从模式串的哪个位置开始重新匹配。

Next数组定义:

  • next[0] = -1

  • next[i] = 长度为i的模式串子串中,使得前缀和后缀相等的最大长度

算法执行流程

  1. 预处理模式串,构建next数组

  2. 使用双指针在主串和模式串中进行匹配

  3. 当字符匹配时,双指针同时前进

  4. 当字符不匹配时,根据next数组回溯模式串指针

函数详解

1. 构建Next数组函数

属性 说明
头文件 #include <stdio.h> #include <string.h> #include <stdlib.h>
函数名 void getNext(char* pattern, int* next)
参数 pattern:模式字符串 next:next数组指针
返回值
参数示例 pattern = "ABABC" next:长度为5的整型数组
示例含义 为模式串"ABABC"构建next数组

2. KMP匹配函数

属性 说明
头文件 #include <stdio.h> #include <string.h> #include <stdlib.h>
函数名 int kmpSearch(char* text, char* pattern)
参数 text:主文本字符串 pattern:要匹配的模式字符串
返回值 匹配成功的起始位置,未找到返回-1
参数示例 text = "ABABDABACDABABC" pattern = "ABABC"
示例含义 在主串中查找模式串"ABABC"

C语言实现

复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void getNext(char* pattern, int* next) {
    int len = strlen(pattern);
    next[0] = -1;
    int i = 0, j = -1;
    while (i < len - 1) {
        if (j == -1 || pattern[i] == pattern[j]) {
            i++;
            j++;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
}

int kmpSearch(char* text, char* pattern) {
    int tLen = strlen(text);
    int pLen = strlen(pattern);
    if (pLen == 0) return 0;
    int* next = (int*)malloc(pLen * sizeof(int));
    getNext(pattern, next);
    int i = 0, j = 0;
    while (i < tLen && j < pLen) {
        if (j == -1 || text[i] == pattern[j]) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    free(next);
    if (j == pLen) {
        return i - j;
    } else {
        return -1;
    }
}

int main() {
    char text[] = "ABABDABACDABABC";
    char pattern[] = "ABABC";
    int pos = kmpSearch(text, pattern);
    if (pos != -1) {
        printf("模式串在主串中的位置: %d\n", pos);
        printf("主串: %s\n", text);
        printf("匹配: ");
        for (int i = 0; i < pos; i++) printf(" ");
        printf("%s\n", pattern);
    } else {
        printf("未找到匹配的模式串\n");
    }
    return 0;
}

代码详细解析

getNext函数解析

复制代码
void getNext(char* pattern, int* next) {
    int len = strlen(pattern);      // 获取模式串长度
    next[0] = -1;                  // 初始化第一个位置
    int i = 0, j = -1;             // i:后缀末尾, j:前缀末尾
    while (i < len - 1) {          // 遍历模式串
        if (j == -1 || pattern[i] == pattern[j]) {
            i++; j++;              // 前后缀匹配成功
            next[i] = j;           // 设置next值
        } else {
            j = next[j];           // 回溯到前一个匹配位置
        }
    }
}

执行示例:

模式串"ABABC"的next数组构建过程:

next[0] = -1

next[1] = 0 (A)

next[2] = 0 (AB)

next[3] = 1 (ABA → 公共前后缀"A")

next[4] = 2 (ABAB → 公共前后缀"AB")

kmpSearch函数解析

复制代码
int kmpSearch(char* text, char* pattern) {
    int tLen = strlen(text);       // 主串长度
    int pLen = strlen(pattern);    // 模式串长度
    if (pLen == 0) return 0;      // 空模式串直接返回
    int* next = (int*)malloc(pLen * sizeof(int));
    getNext(pattern, next);        // 构建next数组
    int i = 0, j = 0;             // i:主串指针, j:模式串指针
    while (i < tLen && j < pLen) {
        if (j == -1 || text[i] == pattern[j]) {
            i++; j++;              // 字符匹配,双指针前进
        } else {
            j = next[j];           // 根据next数组回溯
        }
    }
    free(next);                    // 释放内存
    if (j == pLen) {
        return i - j;              // 返回匹配位置
    } else {
        return -1;                 // 未找到
    }
}

算法思路深度解析

为什么需要KMP算法?

暴力匹配的缺陷:

复制代码
// 暴力匹配示例
int bruteForce(char* text, char* pattern) {
    int tLen = strlen(text);
    int pLen = strlen(pattern);
    for (int i = 0; i <= tLen - pLen; i++) {
        int j;
        for (j = 0; j < pLen; j++) {
            if (text[i + j] != pattern[j]) break;
        }
        if (j == pLen) return i;
    }
    return -1;
}

问题分析:

每次匹配失败,i都要回溯到i+1,j回到0 产生大量重复比较 时间复杂度最坏O(m*n)

KMP的优化思路

关键观察:

当在位置i匹配失败时,text[i-j...i-1]与pattern[0...j-1]是匹配的。

如果pattern[0...k-1] = pattern[j-k...j-1],那么可以直接将pattern向右滑动j-k位。

设计步骤:

  1. 预处理阶段:分析模式串的自相似性,构建next数组

  2. 匹配阶段:利用next数组避免主串指针回溯

  3. 回溯策略:当不匹配时,模式串指针跳转到next[j]位置

Next数组的设计思路

设计目标:

对于每个位置j,找到最大的k(k<j)使得:

pattern[0...k-1] = pattern[j-k...j-1]

实现策略:

使用双指针技术:i遍历模式串,j记录前缀位置

当pattern[i] == pattern[j]时,next[i+1] = j+1

当不相等时,利用已计算的next数组进行回溯

实际应用场景

1. 文本编辑器查找功能

复制代码
// 在文档中查找关键词
char document[] = "这是一个示例文档,包含一些重要的关键词";
char keyword[] = "关键词";
int position = kmpSearch(document, keyword);

2. DNA序列匹配

复制代码
// 在DNA序列中查找特定模式
char dna[] = "ATCGATCGATCGATCG";
char pattern[] = "ATCG";
int matchPos = kmpSearch(dna, pattern);

3. 日志文件分析

复制代码
// 在日志中查找错误信息
char log[] = "系统运行正常,未发现错误,用户登录成功";
char error[] = "错误";
int errorPos = kmpSearch(log, error);

常见面试题

1. 基础概念题

Q1:KMP算法相比暴力匹配的优势是什么?

时间复杂度:KMP为O(m+n),暴力匹配为O(m*n)

空间复杂度:KMP需要O(m)的额外空间存储next数组

适用场景:适合主串远大于模式串的情况

Q2:Next数组的含义和作用是什么?

含义:记录模式串前缀和后缀的最长公共长度

作用:匹配失败时确定模式串指针的回溯位置,避免重复比较

2. 算法实现题

Q3:手动计算模式串"ABCDABD"的next数组

模式串: A B C D A B D

next: -1 0 0 0 0 1 2

Q4:优化next数组(nextval数组)的实现

复制代码
void getNextVal(char* pattern, int* nextval) {
    int len = strlen(pattern);
    nextval[0] = -1;
    int i = 0, j = -1;
    while (i < len - 1) {
        if (j == -1 || pattern[i] == pattern[j]) {
            i++; j++;
            if (pattern[i] != pattern[j]) {
                nextval[i] = j;
            } else {
                nextval[i] = nextval[j];
            }
        } else {
            j = nextval[j];
        }
    }
}

3. 综合应用题

Q5:在大量文本中查找多个模式串的最优方法

  • 使用AC自动机(Aho-Corasick算法),这是KMP算法在多模式匹配上的扩展

Q6:KMP算法在流式数据处理中的应用

  • 维护匹配状态,逐个处理输入字符,适合无法一次性加载全部数据的情况

性能分析与优化

时间复杂度分析

构建next数组 :O(m),其中m为模式串长度 ​​​​​​​ 匹配过程:O(n),其中n为主串长度

总体复杂度:O(m+n)

空间复杂度

next数组 :O(m)总体空间:O(m)

优化建议

  1. nextval数组:避免不必要的回溯

  2. 内存预分配:对于频繁匹配的场景,可预先计算next数组

  3. 多模式匹配:升级到AC自动机

总结

KMP算法通过巧妙的next数组设计,实现了字符串匹配的高效性能。其核心设计思路是利用已匹配信息避免重复比较,这一思想体现在:

  1. 预处理思想:提前分析模式串特征,为匹配阶段做准备

  2. 空间换时间:使用额外空间存储next数组,换取时间复杂度的优化

  3. 指针不回溯:主串指针单向移动,避免重复扫描

理解KMP算法的设计思路不仅有助于掌握该算法,也为学习其他字符串处理算法打下坚实基础。在实际应用中,KMP算法在文本搜索、生物信息学、数据挖掘等领域都有广泛应用。

掌握KMP算法的关键在于深入理解next数组的构建过程和回溯机制,通过多练习、多思考,才能真正掌握这一重要的字符串匹配算法。

相关推荐
-雷阵雨-6 小时前
数据结构——LinkedList和链表
java·开发语言·数据结构·链表·intellij-idea
2401_8414956412 小时前
【数据结构】红黑树的基本操作
java·数据结构·c++·python·算法·红黑树·二叉搜索树
C++chaofan14 小时前
项目中为AI添加对话记忆
java·数据结构·人工智能·redis·缓存·个人开发·caffeine
失散1317 小时前
软件设计师——03 数据结构(下)
数据结构·软考·图论·软件设计师
yiqiqukanhaiba18 小时前
Linux编程笔记2-控制&数组&指针&函数&动态内存&构造类型&Makefile
数据结构·算法·排序算法
高山上有一只小老虎21 小时前
输出单向链表中倒数第k个结点
java·数据结构·链表
Algo-hx21 小时前
数据结构入门 (五):约束即是力量 —— 深入理解栈
数据结构·算法
我要用代码向我喜欢的女孩表白21 小时前
数据结构13003考前急救
数据结构
NiKo_W1 天前
C++ 反向迭代器模拟实现
开发语言·数据结构·c++·stl