KMP算法证明与实现(c++版)

目录

KMP简介:

暴力求解:

KMP算法:

KMP正确性证明:

* 引理 32.5(前缀函数迭代引理)

* 引理 32.6

* 定义

* 推论 32.7

举例说明:

名词解释:

引入 next 表:

next 表说明:

代码举例:

1.构造 next 表

2.KMP算法

完整代码 

KMP简介:

       在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个字符串S内查找一个词W的出现位置。一个词在不匹配时本身就包含足够的信息来确定下一个匹配可能的开始位置,此算法利用这一特性以避免重新检查先前配对的字符。——(维基百科)

用途:字符串匹配问题

提出问题:给你两个数字序列,你的任务时在序列 a 中找到和序列 b 完全匹配的子串,如果有多个匹配的位置,输出最小的那个。设文本串长度为 n,模式串长度为 m

暴力求解:

将模式串与文本串按位进行匹配,当不匹配时,将模式串右移一位,重新匹配,直到匹配成功或遍历完整个文本串。

时间复杂度:O(m*n)

KMP算法:

根据最长相等前后缀,构建 next 表,当不匹配时,按照 next 表进行跳转,重新匹配,直到匹配成功或遍历完整个文本串。

时间复杂度:O(m+n)

KMP正确性证明:

* 引理 32.5(前缀函数迭代引理)

给定长度为 m 的模式 P,其前缀函数 π。证明:对 q=1,2,…,m,
\pi ^{*}[q]={k:k<q , Pk⊐Pq} 。

证:数学归纳证明 \pi ^{*}[q]⊆{k:k<q , Pk⊐Pq} 

当 i=1时,\pi ^{i}[q]=\pi^{1}[q]=π[q],
∵ 由定义 π[q]=max {k:k<q , Pk⊐Pq}
∴ π[q]∈{k:k<q , Pk⊐Pq}

假设,当 i=n−1时,\pi^{n-1} [q]∈{k:k<q , Pk⊐Pq};

当 i=n 时,
∵ P\pi^{n}[q]=Pπ[\pi^{n-1}  [q]] ⊐P\pi^{n-1}  [q] ⊐Pq
∴ P\pi^{n}[q]⊐Pq
∴ \pi^{n}[q]∈{k:k<q , Pk⊐Pq}
∴ \pi ^{*}[q]⊆{k:k<q , Pk⊐Pq} 成立。 

反正法证明 {k:k<q , Pk⊐Pq}⊆\pi ^{*}[q] 

假设 {k:k<q , Pk⊐Pq}⊈\pi ^{*}[q],我们取非空集合 {k:k<q , Pk⊐Pq}−\pi ^{*}[q]中最大的元素 m,
取 n=min {k:k∈\pi ^{*}[q] , k>m}
由上面的证明,我们知道 n∈{k:k<q , Pk⊐Pq}
∴ Pn⊐Pq
∵ Pm⊐Pq , n>m
∴ Pm⊐Pn
∴ m≤π[n]
∵ m∉\pi ^{*}[q],π[n]∈\pi ^{*}[q]
∴ m≠π[n],m<π[n]
∵ π[n]<n
∴ m<π[n]<n,这与 n 和 m 的取法相矛盾(要么 m 不是非空集合 {k:k<q , Pk⊐Pq}−\pi ^{*}[q] 中最大的,要么 n 不是 {k:k∈\pi ^{*}[q] , k>m}中最小的)
∴ 假设不成立,即 {k:k<q , Pk⊐Pq}⊆\pi ^{*}[q]     

综上所述,引理结论成立,证毕。

* 引理 32.6

给定长度为 m 的模式 P,其前缀函数 π。证明:对 q=1,2,…,m,如果 π[q]>0,
那么 π[q]−1∈\pi ^{*}[q−1]。
 

证: 令 r=π[q],那么 r<q, r−1<q−1,Pr⊐Pq
∵ r=π[q]>0
∴ Pr−1⊐Pq−1(通过把 Pr, Pq 的最后一个字符去掉)
∴ π[q]−1=r−1 ∈{k:k<q−1 , Pk⊐Pq−1}

由引理 32.5 可得 π[q]−1 ∈\pi ^{*}[q−1] 

证毕。

* 定义

对 q=2,3,…,m,定义 \pi ^{*}[q−1] 的子集 Eq−1 如下:
Eq−1={ k∈\pi ^{*}[q−1]:P[k+1]=P[q]
        ={ k:k<q−1,Pk⊐Pq−1,P[k+1]=P[q] }(引理 32.5)
        ={ k:k<q−1,Pk+1⊐Pq }
 

* 推论 32.7

给定长度为 m 的模式 P,其前缀函数 π,那么对 q=2,3,…,m,
有 π[q]=        0                             当 Eq−1=∅
                1+max {k∈Eq−1}        当 Eq−1≠∅  。

证: 当 Eq−1=∅ 时,由Eq−1={ k:k<q−1,Pk+1⊐Pq } 知,不存在 k<q−1,k+1<q ,使得 Pk+1⊐Pq
∴π[q]=0

当 Eq−1≠∅ 时,
∵ ∀ k ∈ Eq−1,有 Pk+1⊐Pq
∴ π[q]≥k+1≥max {k∈Eq−1}+1
即 π[q]≥max {k∈Eq−1}+1      ⋯      (1)

另一方面,由上我们知道 π[q]>0,令 r=π[q]−1,由引理 32.6,得 r∈\pi ^{*}[q−1]      ⋯      (2)
∵ r+1=π[q]
∴ Pr+1⊐Pq      ⋯      (3)

由 (2),(3) 两式,我们得到 r∈Eq−1
∴ r≤max {k∈Eq−1}
∴ π[q]−1≤max {k∈Eq−1}
即 π[q]≤max {k∈Eq−1}+1      ⋯      (4)

由 (1),(4) 两式,可得 π[q]=max {k∈Eq−1}+1

综上所述,推论得证。

举例说明:

文本串:aabaabaaf

模式串:aabaaf

分析:将模式串与文本串按位进行匹配,发现 f 与 b 不匹配,KMP算法思路为求取 f 前的字串 aabaa 的最长相等前后缀为 aa,此时跳转到 aa 的下一次字符 b 进行重新匹配。

名词解释:

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

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

最长相等前后缀:找出 P 的所有前缀集合 CP,找出 P 的所有后缀集合 CS,求 CP 和 CS 的交集中长度最大的元素,称之为 P 的最长相等前后缀。

引入 next 表:

1.求最长相等前后缀

模式串前缀后缀最长相等前后缀长度
a0
aaaaa1
aaba,aab,ab

0
aabaa,aa,aaba,ba,abaa1
aabaaa,aa,aab,aabaa,aa,baa,abaaaa2
aabaafa,aa,aab,aaba,aabaaf,af,aaf,baaf,abaaf0

2.构建模式串的 next 表

文本串aabaaf
Index012345
Next010120

next 表说明:

以上述为例,当 f 不匹配时,查取 f 的前一个字符 a 的 Next值为 2,由此跳转至 Index 为 2 的字符 b,此时将从 b 进行重新匹配,直到匹配成功或遍历完整个文本串。

文本串aabaabaaf
模式串aabaaf
文本串aabaabaaf
模式串aabaaf
文本串aabaabaaf
模式串aabaaf
文本串aabaabaaf
模式串aabaaf

代码举例:

说明:将 next[0] 设置为 -1,即如果第一个字符就不匹配,则将模式串右移一位后继续匹配。

问题再现:给你两个数字序列,你的任务时在序列 a 中找到和序列 b 完全匹配的子串,如果有多个匹配的位置,输出最小的那个。

1.构造 next 表

void GetNextTab(int m){
    int j = 0;
    nextTab[j] = -1;
    int i = nextTab[j];
    while(j < m){
    if(i == -1 || pattern[i] == pattern[j]){
        i++;
        j++;
        nextTab[j] = i;
    }
    else i = nextTab[i];
    }
    return;
}

2.KMP算法

int KMP(int n, int m){
    GetNextTab(m);
    int i = 0;
    int j = 0;
    while(i < n && j < m){
        if(j == -1 || text[i] == pattern[j]){ //当前字符串匹配成功
            i++;
            j++;
        }
        else j = nextTab[j];                   //当前字符串匹配失败
    }
    if(j == m) return i - j + 1;               //模式串匹配成功
    else return -1;                            //模式串匹配失败
}

完整代码 

#include<iostream>
#include<cstdio>

using namespace std;

int nextTab[1000];
int text[1000];
int pattern[1000];

void GetNextTab(int m){
    int j = 0;
    nextTab[j] = -1;
    int i = nextTab[j];
    while(j < m){
    if(i == -1 || pattern[i] == pattern[j]){
        i++;
        j++;
        nextTab[j] = i;
    }
    else i = nextTab[i];
    }
    return;
}

int KMP(int n, int m){
    GetNextTab(m);
    int i = 0;
    int j = 0;
    while(i < n && j < m){
        if(j == -1 || text[i] == pattern[j]){ //当前字符串匹配成功
            i++;
            j++;
        }
        else j = nextTab[j];                   //当前字符串匹配失败
    }
    if(j == m) return i - j + 1;               //模式串匹配成功
    else return -1;                            //模式串匹配失败
}

int main(){
    int casenum;
    scanf("%d", &casenum);
    while(casenum--){
        int n, m;
        scanf("%d%d", &n, &m);
        for(int i=0; i<n; ++i) scanf("%d", &text[i]);
        for(int i=0; i<m; ++i) scanf("%d", &pattern[i]);
        printf("%d\n", KMP(n, m));
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值