【数据结构与算法】第17篇:串(String)的高级模式匹配:KMP算法

一、BF算法的问题

回顾BF算法,当匹配失败时:

text

复制代码
主串: a b c a b d
模式: a b d
       ↑ ↑ ✗ (c != d)

此时i回溯到2,j重置为1,重新开始比较。问题是,我们已经知道S2=b,而T1=a,肯定不匹配。BF算法没有利用这个信息,做了很多无用的比较。

KMP的核心思想:当匹配失败时,主串指针i不回溯,只移动模式串指针j,利用已匹配部分的前缀和后缀信息,跳过不必要的比较。


二、前缀、后缀和部分匹配值

2.1 定义

  • 前缀:除最后一个字符外,字符串的所有头部子串

  • 后缀:除第一个字符外,字符串的所有尾部子串

  • 部分匹配值:前缀和后缀的最长相等长度

例如 "ababa":

长度 前缀 后缀 是否相等
1 a a ✓
2 ab ba ✗
3 aba aba ✓
4 abab baba ✗

最长相等长度为3,所以部分匹配值为3。

2.2 计算模式串的部分匹配值

以 T = "abab" 为例:

子串 前缀 后缀 最长相等长度
"a" 空 空 0
"ab" {a} {b} 0
"aba" {a,ab} {a,ba} 1 (a)
"abab" {a,ab,aba} {b,ab,bab} 2 (ab)

得到部分匹配值数组:[0, 0, 1, 2]


三、next数组的推导

3.1 next数组的含义

next[j] 表示:当模式串的第j个字符与主串不匹配时,模式串指针j应该跳到的位置(即下一次从哪个位置开始比较)。

重要约定 :为了方便,我们通常让数组下标从1开始,next[1] = 0 表示特殊情况。

3.2 递推公式

text

复制代码
next[1] = 0
next[2] = 1

当 j > 2 时:
令 k = next[j-1]
如果 T[j-1] == T[k],则 next[j] = k + 1
否则,递归地让 k = next[k],继续比较,直到 k=0

这个递推关系比较抽象,我们通过具体例子来理解。

3.3 手算next数组

例1:T = "abab"

text

复制代码
j=1: next[1] = 0

j=2: next[2] = 1

j=3: 看前一个字符 T[2]=b
     取 k = next[2]=1
     比较 T[2] 和 T[1]: 'b' != 'a'
     让 k = next[1]=0,k=0 停止
     next[3] = k+1 = 1

j=4: 看前一个字符 T[3]=a
     取 k = next[3]=1
     比较 T[3] 和 T[1]: 'a' == 'a',匹配
     next[4] = k+1 = 2

结果:next = [0, 1, 1, 2]

例2:T = "abcabc"

text

复制代码
j=1: next[1]=0
j=2: next[2]=1
j=3: T[2]=b, k=1, T[1]=a, b!=a, k=0 → next[3]=1
j=4: T[3]=c, k=1, T[1]=a, c!=a, k=0 → next[4]=1
j=5: T[4]=a, k=1, T[1]=a, a==a → next[5]=2
j=6: T[5]=b, k=2, T[2]=b, b==b → next[6]=3

结果:next = [0, 1, 1, 1, 2, 3]

3.4 代码实现next数组

c

复制代码
void getNext(const char *T, int *next, int len) {
    int i = 1, j = 0;  // i是模式串当前下标,j是已匹配的前缀长度
    next[1] = 0;
    
    while (i < len) {
        if (j == 0 || T[i-1] == T[j-1]) {  // 注意字符串下标从0开始
            i++;
            j++;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
}

四、KMP匹配算法

4.1 算法流程

有了next数组,KMP匹配就很简洁了:

text

复制代码
i = 1, j = 1
while (i <= S.len && j <= T.len) {
    if (j == 0 || S[i] == T[j]) {
        i++;
        j++;
    } else {
        j = next[j];  // i不回溯,j跳转
    }
}
if (j > T.len) 返回 i - T.len
else 返回 0

4.2 动画演示

以 S = "ababcabc", T = "abcabc" 为例,next = [0,1,1,1,2,3]

text

复制代码
第1轮:i=1,j=1
S: a b a b c a b c
T: a b c a b c
   ↑ ↑ ✗ (a==a, b==b, a!=c)
此时 j=3,根据next[3]=1,j跳到1
i保持=3

第2轮:i=3,j=1
S: a b a b c a b c
T:     a b c a b c
       ↑ ✗ (a!=a? 等等,S[3]=a, T[1]=a,相等!)
等等,仔细看:S[3]=a, T[1]=a,匹配!

实际上,KMP的关键是当不匹配时,j跳转到nextj,继续比较。

4.3 完整代码

c

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

#define MAXLEN 100

// 获取next数组(下标从1开始)
void getNext(const char *T, int *next, int len) {
    int i = 1, j = 0;
    next[1] = 0;
    
    while (i < len) {
        if (j == 0 || T[i-1] == T[j-1]) {
            i++;
            j++;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
}

// KMP算法
int kmp(const char *S, const char *T) {
    int lenS = strlen(S);
    int lenT = strlen(T);
    
    if (lenT == 0) return 1;
    if (lenS < lenT) return 0;
    
    int *next = (int*)malloc((lenT + 1) * sizeof(int));
    getNext(T, next, lenT);
    
    int i = 1, j = 1;  // 从1开始
    while (i <= lenS && j <= lenT) {
        if (j == 0 || S[i-1] == T[j-1]) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    
    free(next);
    
    if (j > lenT) {
        return i - lenT;  // 返回位置(从1开始)
    }
    return 0;
}

// 打印next数组
void printNext(const char *T) {
    int len = strlen(T);
    int *next = (int*)malloc((len + 1) * sizeof(int));
    getNext(T, next, len);
    
    printf("T = \"%s\"\n", T);
    printf("j:  ");
    for (int i = 1; i <= len; i++) {
        printf("%2d ", i);
    }
    printf("\nnext: ");
    for (int i = 1; i <= len; i++) {
        printf("%2d ", next[i]);
    }
    printf("\n\n");
    
    free(next);
}

int main() {
    // 测试next数组
    printNext("abab");
    printNext("abcabc");
    printNext("aaaaa");
    
    // 测试匹配
    printf("--- 匹配测试 ---\n");
    printf("'abcabd' 找 'abd': %d\n", kmp("abcabd", "abd"));
    printf("'hello world' 找 'world': %d\n", kmp("hello world", "world"));
    printf("'aaaaa' 找 'aaa': %d\n", kmp("aaaaa", "aaa"));
    printf("'abc' 找 'def': %d\n", kmp("abc", "def"));
    
    return 0;
}

运行结果:

text

复制代码
T = "abab"
j:   1  2  3  4 
next: 0  1  1  2 

T = "abcabc"
j:   1  2  3  4  5  6 
next: 0  1  1  1  2  3 

T = "aaaaa"
j:   1  2  3  4  5 
next: 0  1  2  3  4 

--- 匹配测试 ---
'abcabd' 找 'abd': 4
'hello world' 找 'world': 7
'aaaaa' 找 'aaa': 1
'abc' 找 'def': 0

五、nextval数组(优化)

5.1 next数组的问题

当 T[j] == T[next[j]] 时,匹配失败后跳转到nextj,但该位置的字符和当前字符相同,仍然会失败,造成额外比较。

例如 T = "aaaab":

text

复制代码
j=4, T[4]=a, next[4]=3, T[3]=a,相同
匹配失败后:j=4 → j=3 → j=2 → j=1,连续跳转多次

5.2 nextval优化

递归地优化:如果 T[j] == T[next[j]],则 nextval[j] = nextval[next[j]]。

计算nextval:

text

复制代码
nextval[1] = 0
for j = 2 to len:
    if T[j] == T[next[j]]:
        nextval[j] = nextval[next[j]]
    else:
        nextval[j] = next[j]

5.3 代码实现

c

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

六、复杂度分析

算法 时间复杂度 空间复杂度
BF O(n×m) O(1)
KMP O(n+m) O(m)(next数组)

KMP的优越性在大规模文本匹配中非常明显。例如:

  • 主串长度 n=1,000,000

  • 模式串长度 m=1,000

  • BF最坏需要 10^9 次比较

  • KMP只需要约 1,001,000 次比较


七、KMP与BF的对比

对比项 BF算法 KMP算法
主串指针 会回溯 不回溯
模式串指针 重置为1 跳转到nextj
预处理 无 计算next数组
时间复杂度 O(n×m) O(n+m)
空间复杂度 O(1) O(m)
适用场景 小规模、简单匹配 大规模、多次匹配

八、小结

这一篇我们学习了KMP算法:

要点 说明
核心思想 主串指针不回溯,利用部分匹配信息移动模式串
next数组 表示匹配失败时j应该跳到的位置
next推导 递推公式,基于前缀和后缀的相等关系
nextval优化 避免连续跳转到相同字符
时间复杂度 O(n+m),比BF的O(n×m)高效很多

KMP的精髓:当匹配失败时,我们已经知道前面哪些字符是匹配的,利用这个信息跳过不必要的比较。

下一篇我们讲数组的压缩存储。


九、思考题

  1. 模式串 "abcaabca" 的next数组是多少?手动推导一下。

  2. KMP算法中,主串指针为什么不回溯?这样会不会漏掉可能的匹配?

  3. 如果模式串是 "abc",它的next数组是多少?匹配 "abcabc" 时,KMP和BF的比较次数分别是多少?

  4. 什么时候用KMP比BF好?什么时候BF其实就够了?

相关推荐
微尘hjx6 小时前
【ByteTrack 04】多轨迹算法ByteTrack改进点
算法·yolo·目标检测·目标跟踪·bytetrack·ultralytics·车跟踪
小向是个Der7 小时前
嵌入式进阶——MCU上电后的动作
c语言·单片机·mcu·学习·链接脚本
路灯上の马陈8 小时前
二叉树的定义与性质
数据结构·二叉树·树结构·1024程序员节
bkspiderx10 小时前
基于Socket的广播通信及C语言实现详解
c语言·开发语言·socket·广播·socket的广播通信
小蒋观天下10 小时前
雷击与浪涌防护:前端SPD选型、接地网误区,接地不当反而引入干扰
深度学习·算法·机器学习
辣知11 小时前
辣知·化智93 被历史尘封的鬻子
学习
Bshaya_11 小时前
红黑树详解:概念、规则与插入实现
数据结构·红黑树·二叉搜索树·平衡树·插入逻辑
WBluuue12 小时前
数据结构与算法:分块(一):块状数组、块状链表
数据结构·c++·算法·链表
每天题库13 小时前
西藏安全员 A 证备考真题题库 强化重点知识刷题提升通过率
学习·安全·考试·题库·考证