KMP字符串匹配算法(二)—KMP要点和证明

KMP字符串匹配算法(二)—KMP要点和证明

在朴素字符串的匹配算法中,查找模式串P在字符串S中的匹配是一种walk-one-by-one的过程,即从S[i]开始匹配,一旦在S[j] (ji+1<P.length) ( j − i + 1 < P . l e n g t h ) 处无法匹配,算法只能前进一步,还得从S[i+1]从头开始匹配……。
KMP字符串匹配算法就比朴素字符串的匹配算法高明,它会事先处理模式串P,充分利用P的信息,做到“jump”式的匹配,就像有限自动机一样,在当前状态,遇到某个输入,跳转到新的状态。
例如: 模式串P=abcabcabfg, 文本串S=abcabcabcabcabfg。
从S[0]开始匹配,可以看到,P[8] = ‘f’ != S[8]=’c’。按照朴素算法,下一次匹配得从S[1]开始。可是我们明明都匹配到了C=abcabcab,重新来过?不甘心!能不能利用C的信息呢?[注:因为C是P的一个前缀,C的所有信息只是P的信息的一个子集,说到底,其实也就是利用模式串P的预处理信息。]
朴素算法walk-one-by-one的方式会把所有的可能解都验证一遍,也就是在做穷举。
观察C,找出一个C的前缀F,使得1).F和C的一个后缀相同;2).满足1)且字符串长度最长。显然F是唯一的:F=abcab。
由于F满足1),所以省去了从头比较的必要,直接从S[8]和P[5]开始比较(前5个字符已对齐,无须比较),节省了比较次数,由于满足2)所以不会跳过任何可能匹配的case,因为如果只是满足1),那F’=ab也满足,直接从S[8]和P[2]开始比较(前2个已对齐,无须比较),但是这样P[0]一下跳了C.length-F’.length= 8-2=6步,从与S[0]对齐一下子跳到了与S[6]对齐,这下直接就跳过了F这种情况[注:对于前缀F,P[0]只跳了C.length-F.length=8-5=3步, 从与S[0]对齐跳到与S[3]对齐],肯定是不对的。

字符串的前缀函数 next[q] n e x t [ q ]

从上面简单的描述中我们发现求出模式串P每一个位置q的前缀 Fq F q 很重要,既然 Fq F q 是P的前缀,那么我们其实只要知道 Fq F q 的长度就行了。这个长度我们管他叫 next[q] n e x t [ q ]

定义函数 next(q)=max{kk<qPkPq} n e x t ( q ) = m a x { k ∣ k < q ∧ P k ⊃ P q } 为字符串 P P 前缀函数
通俗的讲 next[q] n e x t [ q ] 就是能作为 Pq P q 的真后缀的 Pq P q 最长前缀 Pk P k 的长度 k k

P的前缀函数迭代集 next[q] n e x t ∗ [ q ]

next n e x t 迭代得到的集合记为 next[q] n e x t ∗ [ q ] :


next[q]={next[q],next1[q]...nexti[q]...0} n e x t ∗ [ q ] = { n e x t [ q ] , n e x t 1 [ q ] . . . n e x t i [ q ] . . .0 }

                   (1)

其中 nexti[q]=next[nexti1[q]] n e x t i [ q ] = n e x t [ n e x t i − 1 [ q ] ] ,括号中的迭代过程一直进行,直到某一步 nexti[q]=0 n e x t i [ q ] = 0 时停止迭代。

1. 用 P P 的前缀函数对 q q 迭代就能求出 Pq P q 的所有满足 PkPqk<q P k ⊃ P q ∧ k < q 的前缀 Pk P k 的长度 k k (显然 Pk P k 既是 Pq P q 的真前缀又是 Pq P q 的真后缀)
证明: 参见算法导论第三版

集合 Eq1 E q − 1

Eq1 E q − 1 中的元素为 {kP[k+1]=P[q]knext(q1)} { k ∣ P [ k + 1 ] = P [ q ] ∧ k ∈ n e x t ∗ ( q − 1 ) }

怎么理解这个定义呢?这样理解吧:我们把集合 Eq1 E q − 1 看做是从集合 next(q1) n e x t ∗ ( q − 1 ) 筛选一部分值,什么样的值呢?从定义我们知道 next(q1) n e x t ∗ ( q − 1 ) 其实就是满足既能作为串 Pq1 P q − 1 的前缀又能作为它的后缀的所有前缀的长度的集合(长度和前缀是一一对应的关系),再看看集合 Eq1 E q − 1 定义中要求的条件 P[k+1]=P[q] P [ k + 1 ] = P [ q ] ,你想到了什么?

集合 Eq1 E q − 1 中的每一个值加一对应的都是 Pq P q 的一个既能作为 Pq P q 前缀又能作为 Pq P q 后缀的前缀的长度。

故集合 Eq1 E q − 1 中的元素 k k 加1之后对应的前缀 Pk+1 P k + 1 肯定是 Pq P q 的一个后缀,所以有 Pk+1Pqk+1next[q] P k + 1 ⊃ P q ⇒ k + 1 ∈ n e x t ∗ [ q ] 。 可以说 集合 Eq1 E q − 1 和集合 next[q] n e x t ∗ [ q ] 是一一映射的。( Eq1 E q − 1 中的每一个元素 i i 加1之后的值 i+1 i + 1 都会落到集合 next[q] n e x t ∗ [ q ] 中, next[q] n e x t ∗ [ q ] 的每一个元素 i i 减1之后的值 i1 i − 1 都会落到 Eq1 E q − 1 中)。
所以你知道为什么要求集合 Eq1 E q − 1 了吗?因为我们想只通过简单的比较 P[k+1]P[q] 字 符 P [ k + 1 ] 是 否 等 于 字 符 P [ q ] ,从集合 next(q1) n e x t ∗ ( q − 1 ) 推导出集合 next[q] n e x t ∗ [ q ] 。这样就能迭代的求出 next(q) n e x t ( q )

next[q] n e x t [ q ] 理论依据

还记得吗,我们想要求出的是 next(q) n e x t ( q ) 。好了,经过上面的铺陈,现在终于可以心安理得的求 next[q] n e x t [ q ] 了:


next[q]={01+max{kEq1}Eq1=Eq1 n e x t [ q ] = { 0 E q − 1 = ∅ 1 + m a x { k ∈ E q − 1 } E q − 1 ≠ ∅

                   (2)

根据这个结论,在求

next[q] n e x t [ q ] 时,迭代地在

next[q1] n e x t ∗ [ q − 1 ] 中按递减的顺序去寻找一个

k k 满足

kEq1 k ∈ E q − 1 ,这样,如果这样的

k k 存在,那么第一个被找到的

k k 就一定是

Eq1 E q − 1 集合中最大的

k k ,根据等式(2),

next[q]=k+1 n e x t [ q ] = k + 1 ,如果不存在,好办了,根据等式(2)直接得到

next[q]=0 n e x t [ q ] = 0

两点说明:

– 对

next[q1] n e x t ∗ [ q − 1 ] 的迭代顺序按照等式(1)中从左到右的顺序:

i=0,1,... 从 i = 0 , 1 , . . . ,只有这样才能保持迭代得到的待定项

k k 是递减的。



0next[q1] 0 ∈ n e x t ∗ [ q − 1 ] ,所以迭代是良定义的,不用单独把0拿出来考虑。

代码

#include<iostream>
#include<string>

using namespace std;



void ComputeNext(string P,int *next,int const &n){//填上模式串P的next数组

    next[0] = 0;//对应于next[1]=0; 第一个字符的前缀函数值为0.

    int k(0);//k = next[0]=0;
    for (int i(1); i < n; i++){

        //【这里的i对应于第i+1个字符】---所以每一轮循环是在求【next[i+1]】
        while (k>0 && P[i] != P[k]){//初始的 k = next[i-1],由上一轮循环得到【对应于k=next[i]】
            //【这里P[i] != P[k]对应于P[i+1]!=P[k+1]】
                k = next[k];// 【对应于迭代k = next[next[i]]】
        }   

        if (P[i] == P[k])
            k = k + 1; 

        next[i] = k;//同时也是下一轮的k的初始值 k=next[i]

    }

}

/*对于模式串来说,我们会提前计算出每个匹配失败的位置应该移动的距离,花费的时间是常数时间*/
/*在已经匹配的模式串子串中,找出最长的相同的前缀和后缀,然后移动使它们重叠*/
int  KMP_v1(string T,string P){

    int shift(0);//T的与P匹配的第一个子串P'的第一个字符距离起始位置的偏移量。

    int lengthP = P.size();
    int lengthT = T.size();

    int *next = new int[lengthP];


    ComputeNext(P,next, lengthP);//计算前缀函数next


    for (int t(0) ,p(0),index(0); index <lengthT; index=t ){


        if (P[p] == T[t]){
            p++; t++;

            if (p == lengthP){ break; }//找到匹配,退出循环。
        }
        else{
            if (p>0){//P和T已经匹配了一部分字符。

                    p = next[p - 1];//进行下一次比较的P的字符的位置。
                }

            else t++;//P和T没有匹配上任何字符,也就是P和T这一轮第一次字符匹配就失败,这时就要向后移动t

            shift = t - p;//T的下一次比较中与P的首字符对齐的字符距离起始位置的位移量
        }
    }

    return shift;
}


int main(){

    string T;
    string P;

    cout << "输入文本T:" << endl;
    cin >> T;
    cout << "输入模式串P:" << endl;
    cin >> P;

    int s = KMP_v1(T,P);

    if (s < T.length()){
        cout <<P<<" 在 "<<T<<" 中偏移量 "<<s <<" 处"<< endl;
    }
    cout << endl;

    system("pause");
}

至于KMP算法该怎么利用前缀函数 next n e x t ,也就是说求得了 next n e x t 之后要怎么做,怎么利用 next n e x t 来加快匹配速度呢,我相信不做解释,看看代码,在草纸上画画就能明白了。在考虑 next n e x t 的作用时仔细想想它是怎么定义的。

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/unclerunning/article/details/51078425
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞