KMP算法的理解及其C语言的实现

KMP的概念网上有很多介绍,核心是理解PMT(Partial Match Table,部分匹配表),而next数组是为了编程方便将PMT右移一格后得到的。关于PMT的详细概念及最长前、后缀匹配,请参考https://www.zhihu.com/question/21923021/answer/281346746,本文主要参考该文章并分享些自己的见解。

首先要注意理清概念,主串m和匹配串n。KMP的功能是要快速在主串m中找到相应的匹配串n,而next数组的生成(或者是PMT的生成),与主串m无关,所有的操作都是针对匹配串n。很多初学者对于这点比较困惑:我的目的是在主串m中找到匹配串n,怎么关键的next数组的生成会与主串无关呢?其实仔细想想就能明白:既然是“匹配”,我们要抓的是匹配串,匹配串之外的字符毫不相干,匹配串的特性才是我们关心的。

关于字符串的匹配,一般是用两个指针移动来进行的。假设主串m中用指针textIndex,匹配串中用指针patternIndex,在没有任何字符匹配成功之前,patternIndex始终指向匹配串n的第一个字符,而textIndex指针不断右移来寻找匹配的字符。当开始遇到部分匹配的字符串时,patternIndex指针才开始右移。在出现匹配失败之前,两指针的移动方式与一般的匹配算法的移动方式是一致的。而一旦出现匹配失败,如下图所示:

mismatch.png

一般的匹配算法是将patternIndex回退到0,重新指向第一个字符;textIndex也要回到最开始匹配的字符的下一个位置:



然后再让两指针所指向的字符进行对比,因此传统的匹配算法的时间复杂度为O(len(m) * len(n))。

而KMP算法不会让textIndex回退,patternIndex也不是直接回退为0:


这两点是由next数组来保证的。上述例子的PMT和next数组如下所示:


在一般的匹配算法中,如果textIndex也不回退,那上面这个例子将永远找不到正确的结果,这是因为一般匹配算法无法确定从匹配开始到匹配失败这段字符串中有没有“部分匹配串”。所谓“部分匹配串”,就是取该字符串前缀与后缀的最长公共部分,以上面这个例子为例,匹配起始的位置是index 0 (a),匹配失败的位置是index 6 (a != c),其中的“部分匹配串”是从index 2 到 index 5 (即abab),textIndex需要回退重新确定新的匹配起始位置,首先回退到index 1,明显两个指针的值不匹配,然后textIndex指向index 2 (a),匹配成功,确定新的起始位置为index 2,两指针继续移动并匹配……如果index 2 到 index 5不存在“部分匹配串”(比如为cdef),那么textIndex其实也就没必要回退(因为新的匹配起始位置绝对不会出现在当前位置的前面那部分)。

从上述过程看,正是因为一般的匹配算法无法保证匹配起始位置到匹配失败位置之间是否还会出现新的匹配起始位置,才使得textIndex必须回退。如果不想使textIndex回退,该如何解决这个问题呢?用一个数组来记录部分匹配串长度的信息,就可以解决这个问题了,这就是KMP算法。现在我们可以从另一个角度理解next数组的值了,next[i] = 0表示 i 位置前一定不存在新的匹配起始位置,textIndex无需回退;next [i] = 4表示 i 位置前存在新的匹配起始位置,并且到 i 的距离为4,因为已经匹配4个位置了,只是 i 这个位置不知能否匹配,因此textIndex依然无需回退,并且由于next记录的信息,patternIndex也无需回退到0,只需回退到index 为4的位置,大大提升了匹配效率。

在整个KMP匹配的过程中,其实可以看作主串m的后缀与匹配串n的前缀求解公共部分的过程(实质上是匹配串自己的后缀与前缀匹配)。在匹配失败阶段,主串m的新的起始位置未定,而结束位置(匹配失败的位置)已定;匹配串的起始位置已定,而结束位置未定,因此整个过程相当于求解部分匹配串的过程。

关于求解next数组的算法,本质上是求字符串的后缀与前缀的最大公共部分,只需匹配串n错开一位和自己进行比较即可求得。详细代码如下:

Code

int* getNextArray(const char* pattern)
{
    int len = strlen(pattern);

    // Next array records the new beginning index of pattern string if the character is a mismatch between main string and pattern string.
    int* next = (int*)calloc(len, sizeof(int));
    next[0] = -1;

    int i = 0;
    int newStart = -1;       // This variable not only means the new beginning index of pattern string, but also is to traverse the other pattern string.

    // Note that at first, the pattern string will stagger one position and compare itself,
    // then it will get the result which is the intersection of longest prefix substring and longest suffix substring.
    while (i < len - 1)
    {
        if (newStart == -1 || pattern[i] == pattern[newStart])
        {
            next[++i] = ++newStart;
        }
        else
        {
            // If mismatch, then move to proper position according to the next array.
            newStart = next[newStart];
        }
    }
    return next;
}

int kmp(char* text, char* pattern)
{
    int textLen = strlen(text);
    int patternLen = strlen(pattern);
    int textIndex = 0, patternIndex = 0;
    int* next = getNextArray(pattern);

    while (textIndex < textLen && patternIndex < patternLen)
    {
        // Note that "patternIndex = -1" means there is no any matching substring, so both index are all right shift and compare.
        if (patternIndex == -1 || text[textIndex] == pattern[patternIndex])
        {
            ++textIndex;
            ++patternIndex;
        }
        else
            patternIndex = next[patternIndex];
    }

    if (patternIndex == patternLen)
        return textIndex - patternIndex;
    else
        return -1;
}

时间复杂度:O(m + n),其中m为主串的长度,n为匹配串的长度。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355