娓娓道来:什么是KMP算法

     如果你是在电脑前看的这篇文章,请按下CTRL+F键。没错,它弹出了一个有趣的横条。你可以把你想要查找的内容输入到横条中,电脑就会帮你在整个页面中寻找你输入的内容。计算机是怎么帮你找到你想要找的内容的?我们来探讨这个问题。

    如果现在给了我们两个字符串,第一个字符串为主串,第二个字符串为子串,我们需要判断子串是否是主串的一部分,怎么进行判断?从常规的思维出发,我们很容易想到,同时从主串和子串的第一个位置开始比较,如果相等,就比较它们的下一个位置,如果还相等,再比较它们的下一个位置,如果直到字串结束了它们都一一相等,那么就可以得出字串是主串的一部分的结论。如果某一个位置主串和字串对应不相等,就从主串的第下个位置开始与子串的第一个位置进行比较,重复前面的步骤。

   我们可以把两个字符串想象成是两把尺子,上面是主串,下面是子串,最开始把两把尺子的开头对齐,一一比较两把尺子的内容是否相同,如果有不相同的内容,就把下面的尺子(字串)向右移一位,再比较下去。

  

至此,BF算法就闪亮登场了。BF可不是Boyfriend的意思,而是Brute-Force。没错,因为这个算法实在是无比的暴力。

int BF (char* s,char* t,int pos )
{
int i = pos, j = 0;//pos为开始从主串进行搜索的位置,一般置0即可
while (s[i+j] != ‘\0’&&t[j]!=‘\0')
if (s[i+j]== t[j])
j++;// 继续比较后一字符
else
{i++;j = 0;} // 重新开始新的一轮匹配
if(T[j]=='\0')
return i; //匹配成功,返回下标
else
return -1;//匹配失败,返回-1 
}


下面将待检测的子串称为模式串。

现在我们来看下面这个例子:

       主串和模式串前面的5个位置都能一一匹配,直到第6个位置出现不匹配。如果按照BF算法来匹配,下一步模式串T将向右平移一位重新从第一位开始与主串的第二个位置开始比较。如果是你,让你来匹配这两个字符串,你会怎么做呢?

        如果是人工地来匹配这两个式子,人眼的第一步操作也是这样从头一一扫描两个字符串,当扫描到第6个位置不匹配时,人不会傻乎乎的像BF算法那么做,而会在大脑里把把T移待S3的位置对齐进行比较。像下面这样子。

      

      对不对?为什么人的大脑下一步会这样子移位呢?因为S中下一个与T开头位置a相同的元素在S3这里,所以S3之前的位置都不可能与T的第一个位置的元素匹配。这是人眼做出的判断。如果你认识到了人眼的这一步判断,那你也就理解了KMP算法的最最核心和本质的思想了。KMP是联合提出这个字符串模式匹配算法的三个人的名字首字母,第一个K代表的就是图灵奖历史上最年轻的获奖者高德纳。

     KMP核心思想理解起来很简单,但是如何去实现这个思想却是个大问题。我们怎么让计算机带有人眼的功能呢?

     有人会说,这不是很简单吗,从主串从左到右扫描,判断哪个位置和模式串的第一个位置相等不就好了?这当然是可以的,但问题是,一个网页中有成前上万条语句,当我们要匹配的主串数量特别大的时候,我们每次都要对主串进行这样的一次扫描,时间的开销是不可忽视的。既然这样,那模式串是唯一的,我们应该把研究的重点放在模式串上。

   KMP算法的特点是充分的利用和分析了已匹配的字符串的信息。我们不妨看回这个式子:

在我们发现S5和T5不匹配之前,我们发现从S0到S4和T0到T4这之间两个字符串是一一对应相等的。前面我们说了,我们想要找到S中与T0元素相等的元素,那么我们现在锁定T0这个位置,如果T0等于Tx,(0<x<5),那么,Tx=Sx,从而T0=Sx,我们就在S中找到了与T0相等的元素。在上面这个例子中,T0=T3,T3=S3,所以T0=S3,所以把T右移至S3的位置开始比较。也就实现了人眼的判断了。并且,我们还注意到,T0T1和T3T4是对称的(不是中心对称,即abc与cba不对称,abc与abc对称)。既然如此,T0T1=T3T4,T3T4=S3S4,所以T0T1=S3S4,所以当平移到以下的位置后:

不用浪费时间去比较S3S4与T0T1是否相等,直接开始判断S5和T2是否相等就可以了。如果你还不太明白,那我们通过一个更加形象的图示来说明:

左右两边的绿色代表对称的部分。下面进行移位:

移位后从红色部分开始比较:

通过刚才的讲述,我们发现,找到模式串中的这种对称关系至关重要,一旦找到了这种对称关系,可以直接利用一些连续的等式关系,找到新的对齐方式,而且在对齐了之后还能跳过一些字符的判断,从而节省时间。具体来说,拿上面这个例子做比方,T0T1和T3T4为对称关系,对称个数为2,那么,下一个匹配的位置就是T2(也就是c)与S5开始进行配对。也就是说:若对称的字符数为n,那么下一个配对的位置就是Tn与S失配的位置进行配对。根据以上的思想,我们已经可以写出KMP算法的大体代码了:

int KMP(char *s,char *p)//s为主串,p为模式串
{
    int next[strlen(p)];
    int i,j;
    i=0;
    j=0;
    getNext(p,next);      //得到模式串的next数组
    while(i<strlen(s))
    {
        if(j==-1||s[i]==p[j])
        {
            i++;
            j++;
        }
        else
            j=next[j];       
        if(j==strlen(p))
            return i-j;//返回匹配的初始位置
    }
    return -1;
}

你一定注意到了那个next[]数组的存在,它决定了模式串在上一次匹配失败后的移动位置。前面我们已经分析了,模式串中的对称关系至关重要,而这个next[]数组恰恰就是用来反映这种对称关系的,且是来反映对称字符的个数的。具体的定义是这样的:next[j]代表着:

1)next[0]=-1;

2) next[j]=max(k):0<k<j P[0....K-1]=P[j-k,....j-1]

3) next[j]=0 其他情况(即没有对称的关系出现)

简单来说,next[]数组的意义就是失配位置之前的前缀和后缀的最长公有长度(也就是我之前说的,对称的长度)

   ”前缀“和”后缀“。前缀指除了最后一个字符以外,一个字符串的全部头部组合;后缀指除了第一个字符以外,一个字符串的全部尾部组合。下面用一张图来说明:


比方说下面这个模式串的next[]数组值如下:

  P      a    b   a    b   a

 j      0    1   2    3   4

 next    -1   0   0    1   2

你可能会说了,第二条规定我能明白,是反映了对称的关系,那么第一条规定为什么next[0]=-1这个特别的数字?为什么要这么规定?先别着急,我在后面会解释。现在我们把重心放在第二条最重要的规矩上,然后我们把这个求next数组的函数写出来:设next[j]=k,由于已经给了next[0]的初值,那么我们这里用一种类似于数学归纳法的思想,即由第一个数字,递推出后面的数字。这里用了一种继承的思想。具体来说是这样的:

         当前面的字符的前一个字符的对称程度为0时,只要将当前字符与字串第一个字符进行比较。因为如果前面是0,说明前面没有前后对称的序列如果多加了一个字符,要对称的话最多是和第一个字符对称。比方说上面的这个ababa的模式串,P[2]的next值为0,现在要判断P[3]的next值,只需拿P[2]与第一个字符a进行比较,我们发现它们相等,所以P[3]的next值就在P[2]=0的基础上加1,所以是1。依次类推,如果前面的next值是2,那么就拿前面的最后一个字符与模式串开头的第二个字符进行比较,如果相等,那么当前位置的next值就加1,所以为2。上面的P[4]的next值为2就是这样推出来的。

        那么就这样一直递推下去了,现在问题来了,我们不可能一直这样递增下去,总有对称中止的时候,这时候该怎么办呢?

        我们这样想,对称的递增关系中止了,说明对称的个数不会再增加了,那么对称程度肯定比前面的对称要小。那么怎么找到这个更小的对称呢?我们现在回过头来看我们之前提到的,当主串和匹配串进行匹配时,如果在某个位置失配了,我们经过分析后得出的结论是j=next[j](如果不明白请回头看前面的分析),现在我们要找字串(匹配串)中的对称序列,那么是不是相当于是匹配串自己与自己进行自我匹配呢?这个时候,匹配串既是主串也是子串,它自己与自己进行匹配,来找对称的序列。

       设next[j]=k,由上面的思想,我们可以写出得到next数组的函数了:

void getNext(char *p,int *next)//p为模式串
{
    int j,k;
    next[0]=-1;
    j=0;
    k=-1;
    while(j<strlen(p)-1)//注意这里的next数组的大小要-1,因为第一位的next值已经确定了是-1
    {
        if(k==-1||p[j]==p[k])    //匹配的情况下,p[j]==p[k],p[k]表示前缀,p[j]表示后缀
        {
            j++;
            k++;
            next[j]=k;//相当于是next[j+1]=k+1,k是前一个位置的next值
        }
        else                   //p[j]!=p[k]
            k=next[k];  //进行自我匹配
    }
}
      我前面留了为什么next[0]=-1的问题。事实上,我还没有在一本教科书上找到对这一点的具体解释。但是我这里尝试给出我自己的理解,我们不妨来看next[1],根据前缀和后缀的定义,我们发现next[1]一定是为0的,因为如果一个字符串只有两个字符那么它是没有对称的序列(即公共前后缀序列)的,所以next[1]=0,为了使next[1]=0,根据上面的这个得到next数组的函数,我们发现如果令next[0]=-1,那么正好可以使next[1]=0,所以要那么规定。当然这只是我个人的合理猜想,如果有更官方的回答大家可以告诉我。

    至此我们可以说我们已经彻底实现了KMP算法了。但是这个算法可以有优化的地方。

    我们来看下面这个模式串匹配的例子:

 两个字符串进行匹配,发现c和b失配了,于是执行k=next[k[,即k=1,所以来到下一步:

我们发现c和b又失配了,为什么呢,因为当p[j]!=s[i]时,下次就进行p[ next [ j ] ]与s[i]的匹配,如果p[j]=p[ next [ j ] ],那下一步的匹配肯定是失败的,所以我们要防止p[j]=p[ next [ j ] ]情况的出现。如果出现了怎么办?出现了就继续递归下去。由这个思想,我们得到了改进的next数组求法:

void getNext(char *p,int *next)//p为模式串
{
    int j,k;
    next[0]=-1;
    j=0;
    k=-1;
    while(j<strlen(p)-1)
    {
        if(k==-1||p[j]==p[k])    //匹配的情况下,p[j]==p[k]
        {   
                
            j++;
            k++;
           if (p[j] != p[k])  
                next[j] = k;   
            else  
                //因为不能出现p[j] = p[ next[j]],所以当出现时需要继续递归,k = next[k] 
                next[j] = next[k]; 
        }
        else                   //p[j]!=p[k]
            k=next[k];
    }
}
      参考资料:

    http://www.cnblogs.com/dolphin0520/archive/2011/08/24/2151846.html

     http://blog.csdn.net/Oneil_Sally/article/details/3440784

   http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
     http://blog.csdn.net/v_july_v/article/details/7041827

    感谢以上文章给我带来的思维的启发以及形象的图片素材的提供。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值