快速模式匹配算法(KMP)的深入理解

所属分类: 软件编程 / C 语言 阅读数: 128
收藏 0 赞 0 分享
恐怕现在用过电脑的人,一定都知道大部分带文本编辑功能的软件都有一个快捷键ctrl+f 吧(比如word)。这个功能主要来完成“查找”,“替换”和“全部替换”功能的,其实这就是典型的模式匹配的应用,即在文本文件中查找串。

1.模式匹配
模式匹配的模型大概是这样的:给定两个字符串变量S和P,其中S成为目标串,其中包含n个字符,P称为模式串,包含m个字符,其中m<=n。从S的给定位置(通常是S的第一个位置)开始搜索模式P。如果找到,则返回模式P在目标串中的位置(即:P的第一个字符在S中的下标)。如果在目标串S中没有找到模式串P,则返回-1.这就是模式匹配的定义啦,下面来看看怎么实现模式匹配算法吧。

2.朴素的模式匹配
朴素的模式匹配算法非常简单,容易理解,大概思路是这样的:从S的第一个字符S0开始,将P中的字符依次和S中字符比较,若S0=P0 && …… && Sm-1 = Pm-1,则证明匹配成功,剩下的匹配无需进行了,返回下标0。若在某一步Si != Pi 则P中剩下的字符也不用比较了,不可能匹配成功了,然后从S中第二个字符开始与P中第一个字符进行比较,同理,也是知道Sm = Pm-1或者找到某个i使得Si != S-1为止。依次类推若知道以S中第n-m个开始字符为止,还没有匹配成功则证明S中不存模式P。(想想为什么这里强调是n-m)这个代码实现应该是非常简单的,具体开始参考strstr函数的内部实现。可以看看百度百科,给个链接http://baike.baidu.com/view/745156.htm,这里不写出来了,还得赶紧进入正题KMP呢。

3.快速模式匹配算法(KMP)
朴素的模式匹配效率不高的主要原因是进行了重复的字符比较。下一次比较和上一次比较没有任何的联系,是朴素模式匹配的缺点,其实上一次比较的比较结果是可以利用的,这就产生了快速模式匹配。在朴素的模式匹配中,目标串S的下标移动是一步一步的,这其实并不好,移动步数没有必要为1。
现在不妨假设,当前匹配情况是这样的:S0 …… St St+1 …… St+j  与 P0 P1…… Pj ,现在正在尝试匹配的字符是St+j+1和Pj+1,并且St+j+1 != Pj+1,言外之意就是说St St+1……St+j和P0 P1……Pj是完全匹配的。那么这个时候,S中下一次匹配开始位置应该是什么呢??按照朴素的模式匹配,下次比较应该从St+1开始,并且令St+1和P0比较,但是在快速模式匹配中并不是这样,快速模式匹配选择St+j+1和Pk+1比较,K是什么呢?K是这样的一个值,使得P0 P1……Pk 和 Pj-k Pj-k+1……Pj完全匹配,不妨设k=next[j],因此P0 P1……Pk和St+j-k St+j-k+1 ……St+j完全匹配。那么下一次要进行匹配的两个字符应为St+j+1和Pk+1。S和P都没有回溯到下标0在进行比较,这就是KMP之所以快的原因啦。
现在关键问题来了,这个K怎么能得到呢?如果得到这个K值复杂度高,那这个思路就不好了,其实这个K呢,只和模式串P有关系,并且要求m个k,k = next[j],因此只要算一次存储到next数组中就可以了,并且时间复杂度和m有关系(线性关系)。看看具体怎么求next数组的值,即求k。
用归纳法求next[]:设next(0) = -1,若已知next(j) = k,欲求得next[j+1]。
(1)如果Pk+1 = Pj+1,显然next[j+1] = k+1.如果Pk+1 != Pj+1,则next[j+1] < next[j],于是寻找h < k 使得P0 P1……Ph = Pj-h Pj-h+1……Pj = Pk-h Pk-h+1……Pk。也就是说h = next(k);看出来了吧,这是个迭代的过程。(也就是以前的结果对求以后的值有用)
(2)如果不存这样的h,说明P0 P1……Pj+1中没有前后相等的子串,因此next[j+1] =-1.
(3)如果存在这样的h,继续检验Ph和Pj是否相等。知道找到这中相等的情况,或者确定为-1求next[j+1]的过程结束。
看看实现的代码:
复制代码 代码如下:

View Code
int next[20] ={0};
//注意返回结果是一个数组next,保存m个k值得地方,即若next[j]=k
//则str[0]str[1]…str[k] = str[j-k]str[j-k+1]…str[j]
//这样当des[t+j+1]和pat[j+1]匹配失败时,下一个匹配位置为des[t+j+1]和next[j]+1
void Next(char str[],int len)
{
    next[0] = -1;
    for(int j = 1 ; j < len ; j++)
    {
        int i = next[j-1];
        while(str[j] != str[i+1] && i >= 0)//迭代的过程
        {
            i = next[i];
        }
        if(str[j] == str[i+1])
        {
            next[j] = i+1;
        }
        else
        {
            next[j] = -1;
        }
    }
}

现在有了next数组保存的k值,就可以实现KMP算法了:
复制代码 代码如下:

View Code
//des是目标串,pat是模式串,len1和len2是串的长度
int kmp(char des[],int len1,char pat[],int len2)
{
    Next(str2,len2);
    int p=0,s=0;
    while(p < len2  && s < len1)
    {
        if(pat[p] == des[s])
        {
            p++;s++;
        }
        else
        {
            if(p==0)
            {
                s++;//若第一个字符就匹配失败,则从des的下一个字符开始
            }
            else
            {
                p = next[p-1]+1;//用失败函数确定pat应回溯到的字符
            }
        }
    }
    if(p < len2)//整个过程匹配失败
    {
        return -1;
    }
    return s-len2;
}

时间复杂度:
对于Next函数近似接近O(m),KMP算法的时间复杂度为O(n),所以整个算法的时间复杂度为O(n+m)
空间复杂度:
多引入了O(m)的空间复杂度。
4.应用KMP的一道面试题
给定两个字符串是s1和s2,要判定s2是否能够被s1做循环移位得到的字符串包含。例如s1=AABCD,s2 =CDAA,返回true,因为s1循环移位可以变成CDAAB。给定s1=ACBD和s2=ACBD则返回false。
分析:不难发现对s2移位得到的字符串都将是字符串s1s1的子串,如果s2可以有s1循环移位得到,那么s2一定是s1s1的子串,这时KMP算法是不是就很管用了呢。
更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多