码迷,mamicode.com
首页 > 编程语言 > 详细

KMP子串查找算法

时间:2018-02-11 21:25:25      阅读:167      评论:0      收藏:0      [点我收藏+]

标签:图片   str   字符   rri   技术分享   穷举   alt   null   操作   

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法,由他们的名字首字母组成)。

KMP算法的关键是利用已经部分匹配的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。

在介绍KMP之前先说一说朴素解法,也就是最简单的暴力解法,朴素解法是采用穷举的方式一个一个对比达到查找的功能,子串与目标匹配失败后又要重新从子串的开头继续匹配,不考虑重复判断过的字符,所以效率非常的差劲。

下面是朴素解法的实现代码:

int sub_str_index(const char* s, const char* p)   // s是源串,p是模式串、子串
{
    int ret  = -1;                    // ret记录返回值,初始化为-1表示没有找到
    int sl = strlen(s);
    int pl = strlen(p);
    int len  = sl - pl;                 // len记录源串比子串长多少,用于判断子串最多可向后移动的位数,避免匹配过程中剩余源串长度不足

    for(int i=0; (ret<0) && (i<=len); i++)     // 如果没有找到匹配的,且源串还没有到越界下标
    {
        bool equal = true;              // equal用于记录临时的匹配情况,为了下面的循环,默认为真
        
        for(int j=0; equal && (j<pl); j++)     // 如果equal为真且子串没有越界就继续循环 
            equal = (s[i + j] == p[j]);      // 如果当前源串的某个字符与子串对应的字符匹配,equal就为真

        ret = (equal ? i : -1);           // 如果子串全部匹配成功就返回匹配源串首字符的下标,否则返回-1表示失配
    }
    
    return ret;
}

 

接下来介绍KMP算法:

KMP算法是利用已知的信息减少无效匹配判断的一种算法。

这是阮一峰的讲解,我觉得非常好,供参考:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

另外youtube上的黄浩杰也讲的不错,方便的朋友可以去看看。

下面这张图来自阮一峰博客,我做了一些修改,用它来举例子:

技术分享图片

经过几次查找后,这里子串的D和源串的空格失配了,为了提高查找效率,我们发现直接把子串向后移动4位可以更高效的匹配,因为源串当前的3个字符都不能和子串匹配,从而省去了前面3次匹配的过程,实现效率的提升。

那我们如何知道要移动多少位呢,该如何计算呢。

这里就要用到KMP算法的部分匹配表了,部分匹配表用于记录『子串某个字符作为后缀的最后一个字符时』与『以子串第一个字符作为前缀』最多能匹配的字符个数;通过这个计数我们就能知道移动的位数了,因为它记录了当前字符最长匹配的字符个数,所以得出下面的公式:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

前缀是指除最后一个字符外所有的字符组合,而后缀则相反。例如上面的例子,对于子串而然:

由于我们匹配的时候是先匹配子串的第一个字符,成功匹配后向后移动匹配第二个,以此类推,所以我们的部分匹配表必须算出每个字符对应的部分匹配表。

下面来看一张图片

技术分享图片

首先初始化匹配值为0,每次匹配的源串下标为上一个子串字符的部分匹配值,因为该值记录的永远是最长部分匹配值,所以只需要从已经匹配的源串后面继续即可,由于这个值是从1开始的,而下标是从0开始的,所以它指向的是已匹配源串字符的下一个。

下标0字符A没有可匹配的,所以直接设置为0。

下标1字符B没法和下标0字符匹配所以也是0。

下标2字符C不能和下标0字符匹配也不能以BC和AB匹配,所以也为0。

下标3字符D不能和下标0字符匹配,也不能CD与AB匹配,BCD和ABC也没法匹配,结果也是0。

下标4字符A可以和下标0字符匹配,所以匹配值加1,后续的组合再也不能匹配,所以第五个字符A的部分匹配值不再改变,结果为1。

下标5字符B因为之前的下标4字符匹配成功,所以要和下一个源串字符B进行匹配,这里匹配成功,匹配值在上一个字符匹配值的基础上加1变成2,而后续的组合不能再匹配,所以最终结果为2。

下标6字符E与前一个字符的部分匹配值2记录的字符(下标为部分匹配值-1)B不匹配,所以部分匹配值被重置为下标2-1处字符B的部分匹配值,也就是0;E与字符B的部分匹配值记录的字符A也不匹配,所以部分匹配值不变,仍为0。

以此类推下标7处的字符匹配成功,加1。。。。

最后下标13处的字符A与前一个字符B的部分匹配值记录的字符B(下标为6-1)不匹配,部分匹配值被重置为下标6-1处字符的部分匹配值2;下标13处字符A再与下标2处的字符C匹配又失败匹配值被重置为下标2处的部分匹配值0;下标13处字符A与下标0处的字符匹配成功,部分匹配值加1,最终结果为1。

网上很多人把这个部分匹配表写成next数组,我这里根据老师的代码写成了int类型的指针,用堆空间记录,长度与字符长度一致,每个单位为int类型,等效于int数组。

int* make_pmt(const char* p)·                     // 部分匹配表查找函数
{
    int len = strlen(p);
    int* ret = static_cast<int*>(malloc(sizeof(int) * len));  // 申请堆空间用于记录部分匹配表

    if( ret != NULL )                         // 只有堆空间申请成功才能操作
    {
        int ll = 0;                           // ll==>longest length,最长部分匹配值

        ret[0] = 0;                          // 第一个元素没有匹配的所以直接写为0

        for(int i=1; i<len; i++)
        {
            while( (ll > 0) && (p[ll] != p[i]) )          // 部分匹配成功后又出现失败的
            {
                ll = ret[ll-1];                   // 把ll重置为上一个字符部分匹配值(也就是当前ll值)对应的字符(第ll个字符)的部分匹配值,注意下标从0开始所以要减1,注意小写ll和数字1的区别。
            }

            if( p[ll] == p[i] )
            {
                ll++;                        // 如果匹配成功,ll值加1;
            }

            ret[i] = ll;                      // 进入下一轮循环前要把ll值保存进部分匹配表
        }
    }

    return ret;
}

下面是kmp函数:

int String::kmp(const char* s, const char* p)
{
    int ret = -1;
    int sl = strlen(s);
    int pl = strlen(p);
    int* pmt = make_pmt(p);  // 获取子串的部分匹配表,注释该函数返回的是堆空间,用完需要释放

    if( (pmt != NULL) && (0 < pl) && (pl <= sl) )  // 只有当部分匹配表获取成功、子串长度大于0且不超过源串长度的时候才需要计算
    {
        for(int i=0, j=0; i<sl; i++)          // for初始化i和j变量,i用于遍历每个源串,j用于记录已匹配的子串字符数
        {

            while( (j > 0) && (s[i] != p[j]) )    // 有已匹配字符但是后续字符又匹配失败时
            {
                j = pmt[j-1];              // 移动子串,使子串第一个字符对齐到源串下一个匹配的字符位置,这是一次性移动,不再是暴力搜索中的每次只移动一次
            }

            if( s[i] == p[j] )             // 如果匹配成功,记录匹配子串字符个数的j加1
            {
                j++;
            }

            if( j == pl )                // 如果匹配的子串字数与子串长度相等说明匹配成功
            {
                ret = i + 1 - pl;           // 返回匹配源串的第一个字符的下标并跳出循环结束寻找;此时i指向的是匹配的源串的最后一个字符,减去子串长度后等于第一个匹配的字符的前一个,所以要加1
                break;
            }
        }
    }

    free(pmt);

    return ret;
}

 

KMP子串查找算法

标签:图片   str   字符   rri   技术分享   穷举   alt   null   操作   

原文地址:https://www.cnblogs.com/Dua677/p/8442248.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!