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

一、BF算法的问题

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

text

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

此时i回溯到2,j重置为1,重新开始比较。问题是,我们已经知道S[2]=b,而T[1]=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跳转到next[j],继续比较。

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]] 时,匹配失败后跳转到next[j],但该位置的字符和当前字符相同,仍然会失败,造成额外比较。

例如 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 跳转到next[j]
预处理 计算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其实就够了?

相关推荐
大萌神Nagato2 小时前
力扣HOT100 Q146LRU缓存
算法·leetcode·缓存
源码之家2 小时前
大数据毕业设计汽车推荐系统 Django框架 可视化 协同过滤算法 数据分析 大数据 机器学习(建议收藏)✅
大数据·python·算法·django·汽车·课程设计·美食
每天回答3个问题2 小时前
LeetCodeHot100|对称二叉树、二叉树的直径、二叉树的层序遍历
数据结构·c++·ue4·
nianniannnn2 小时前
力扣 3.无重复字符的最长子串
c++·算法·leetcode
小碗羊肉2 小时前
【数据结构】平衡二叉树的旋转机制
数据结构·二叉树
always_TT2 小时前
指针与结构体:链表节点设计
数据结构·链表
IT大师兄吖2 小时前
flux-2-Klein-BFS-换头换脸工作流 懒人整合包
算法·宽度优先
水饺编程2 小时前
第4章,[标签 Win32] :SysMets3 程序讲解02,iVertPos
c语言·c++·windows·visual studio
波哥学开发2 小时前
深入解析 BEV 图像色彩调整与伪彩色映射:从直方图统计到着色器实现
算法·图形学