算法学习笔记 - 字符串匹配(KMP匹配)

前言

KMP 算法,又称模式匹配算法,能够在线性时间内判定字符串 A[1~N] 是否为字符串 B[1~M] 的子串,并求出字符串 A 在字符串 B 中各次出现的位置。

最朴素的做法是,尝试枚举字符串 B 中的每个位置 i,把字符串 A 与字符串 B 的后缀 B[i~M] 对齐,向后扫描逐一比较 A[1] 与 B[i],A[2] 与 B[i+1]...是否相等。我们把这种过程称为 A 与 B 尝试进行“匹配”。这个时间复杂度是 O(NM) 的。

具体步骤

  1. 对字符串 A 进行自我“匹配”,求出一个数组 next,其中 next[i] 表示“ A 中以 i 结尾的非前缀子串”与“ A 的前缀”能够匹配的最长长度,即: next[i]=max{j},其中 j < i 并且                        A[i-j+1~i] = A[1~j] 特别地,当不存在这样的 j 时,令 next[i]=0。
  2. 对字符串 A 与 B 进行匹配,求出一个数组 f,其中 f[i] 表示”B 中以 i 结尾的子串”与“A 的前缀”能够匹配的最长长度,即: f[i]=max{j},其中 j ≤ i 并且 B[i-j+1~i] = A[1~j]。

下面讨论 next 数组的计算方法。根据定义,next[1]=0。接下来我们按照 i = 2~N 的顺序依次计算 next[i]。

假设 next[1~i-1] 已经计算完毕,当计算 next[i] 时,根据定义,我们需要找出所有满足 j

使用朴素算法计算 next 数组

朴素的做法是枚举 j∈[1,i-1] ,并检查 A[i-j+1~i] 与 A[1~j] 是否相等。该算法对每个 i 枚举了 i-1 个非前缀子串,并检查与对应前缀的匹配情况,时间复杂度不会低于 O(N^2)。能否更快地求出 next 呢?

引理

若 j 是 next[i] 的一个“候选项”,即 j < i 且 A[i-j+1~i] =A[1~j],则小于 j 的最大的 next[i] 的“候选项“是 next[i] 的”候选项“是 next[j]。换言之,next[j]+1~j-1 之间的数都不是 next[i] 的”候选项“。

使用优化的算法计算 next 数组

根据引理,当 next[i-1] 计算完毕时,我们即可得知 next[i-1] 的所有“候选项”从大到小依次是  next[i-1],next[next[i-1]],next[next[next[i-1]]]...而如果一个整数 j 是next[i] 的“候选项”,那么 j-1 显然也必须是 next[i-1] 的“候选项”(两个字符串 A[i-j+1~i] 和 A[1~j] 相等的前提是 A[i-j+1~i-1] 和 A[1~j-1] 相等)。因此,在计算 next[i] 时,只需把 next[i-1]+1,next[next[i-1]]+1,next[next[next[i-1]]]+1...作为 j 的选项即可。

具体代码实现

KMP 算法 next 数组的求法

  1. 初始化 next[1] = j = 0,假设 next[1~i-1] 已求出,下面求解 next[i]
  2. 不断尝试扩展匹配长度 j,如果扩展失败(下一个字符不相等),令 j 变为 next[j],直至 j 为 0(应该重新从头开始匹配)。
  3. 如果能够扩展成功,匹配长度 j 就增加 1。 next[i] 的值就是 j。
next[1]=0;
for(int i=2,j=0;i<=n;i++){
    while(j>0&&a[i]!=a[j+1]) j=next[j];
    if(a[i]==a[j+1]) j++;
    next[i]=j;
}

KMP 算法 f 数组的求法

for(int i=1,j=0;i<=m;i++){
    while(j>0&&(j==n||b[i]!=a[j+1])) j=next[i];
    if(b[i]==a[j+1]) j++;
    f[i]=j;
}

这就是 KMP 模式匹配算法。在上面代码的 while 循环中,j 的值不断减小,j=next[j] 的执行次数不会超过每层 for 循环开始时 j 的值与 while 循环结束时 j 的值之差。而在每层 for 循环中,j 的值值至多增加 1.因为 j 始终非负,所以在整个计算过程中,j 减小的幅度总和不会超过 j 增加的幅度总和。故 j 的总变化次数至多为 2(N+M)。整个算法的时间复杂度为 O(N+M)。

你可能感兴趣的:(算法学习笔记,算法,c++)