KMP算法学习札记

参考文章

知乎:如何更好的理解和掌握 KMP 算法?
从头到尾彻底理解KMP
KMP 算法(1):如何理解 KMP
(原创)详解KMP算法
字符串匹配——BMH算法

1. 引言

真的是遇事不决问知乎啊,知乎上对于KMP的理解提供了很多文章,July这篇文章比较通俗易懂,需要的可以去看原文,我想按照原文的逻辑顺序梳理一下我原来不懂的地方。

2. 暴力匹配算法

简单来说就是一个个字的比较,让谁来写都是这个样。重点是这句话

  • 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符
  • 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

这就相当于说要回溯m次,每次最多比较n回,时间复杂度就是O(mn),KMP就是通过少回溯来使得时间复杂度变为O(m+n)。

3. KMP算法

3.1 定义

一个基本事实是,当空格与 D 不匹配时,你其实是已经知道前面六个字符是 "ABCDAB"。KMP 算法的想法是,设法利用这个已知信息,不要把 "搜索位置" 移回已经比较过的位置,而是继续把它向后移,这样就提高了效率。

3.2 步骤

next数组的其实就是在 前缀后缀最长公共元素的基础上得出来的,这个数组代表了当这一位失配后,模式串的指针应该调到哪一位(即next[i]中的值),当这个值是-1的时候,表示目标串指针要向后移动,模式串的指针回溯到0。

3.3 通过代码递推计算next 数组

void GetNext(char* p,int next[])  
{  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1)  
    {  
        //p[k]表示前缀,p[j]表示后缀  
        if (k == -1 || p[j] == p[k])   
        {  
            ++k;  
            ++j;  
            next[j] = k;  
        }  
        else   
        {  
            k = next[k];  
        }  
    }  
}  

问题的关键应该是如何用代码求得next数组,显然这里用到了类似于统计归纳法的证明方法,f(n)可以通过f(n-1)推导出来,关键是这句话

  • 若p[i] == p[j],则next[i + 1] = next [i] + 1 = j + 1;
  • 若p[i] ≠ p[j],如果此时p[ next[j] ] == p[i],则next[ i + 1 ] = next[j] + 1,否则继续递归前缀索引 j = next[j],而后重复此过程。

这里还是拿图比较明白

假设 i 和 j 的位置如上图,由next[i] = j得,也就是对于位置 i 来说,区段 [0, i - 1] 的最长相同真前后缀分别是 [0, j - 1] 和[i - j, i - 1],即这两区段内容相同。
按照算法流程,if (P[i] == P[j]),则i++; j++; next[i] = j;相当于得到了next[i+1]的值。
若不等,则j = next[j],见下图1/2/3:
图1
next[j]代表 [0, j - 1] 区段中最长相同真前后缀的长度。如图,用左侧两个椭圆来表示这个最长相同真前后缀,即这两个椭圆代表的区段内容相同;同理,右侧也有相同的两个椭圆。所以 else 语句就是利用第一个椭圆和第四个椭圆内容相同来加快得到 [0, i - 1] 区段的相同真前后缀的长度。
图2

图3 这个图要分开左右两半看

4. KMP算法的优化

这部分优化优化的是p[j] = p[ next[j] ]的问题。当p[j] != s[i] 时,下次匹配必然是p[ next [j]] 跟s[i]匹配,如果p[j] = p[ next[j] ],必然导致后一步匹配失败。如果出现了p[j] = p[ next[j] ]则需要再次递归,即令next[j] = next[ next[j] ],从而得到了完整的KMP算法。

//优化过后的next 数组求法  
void GetNextval(char* p, int next[])  
{  
    int pLen = strlen(p);  
    next[0] = -1;  
    int k = -1;  
    int j = 0;  
    while (j < pLen - 1)  
    {  
        //p[k]表示前缀,p[j]表示后缀    
        if (k == -1 || p[j] == p[k])  
        {  
            ++j;  
            ++k;  
            //较之前next数组求法,改动在下面4行  
            if (p[j] != p[k])  
                next[j] = k;   //之前只有这一行  
            else  
                //因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归,k = next[k] = next[next[k]]  
                next[j] = next[k];  
        }  
        else  
        {  
            k = next[k];  
        }  
    }  
}  

5. BM算法

KMP的匹配是从模式串的开头开始匹配的,而1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法。该算法从模式串的尾部开始匹配,且拥有在最坏情况下O(N)的时间复杂度。在实践中,BM算法不仅效率高,而且构思巧妙,容易理解。

BM算法定义了两个规则:

坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为-1。
好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。

每次后移这两个规则之中的较大值。这两个规则的移动位数,只与模式串有关,与原文本串无关。

下面举例说明BM算法。例如,给定文本串“HERE IS A SIMPLE EXAMPLE”,和模式串“EXAMPLE”,现要查找模式串是否在文本串中,如果存在,返回模式串在文本串中的位置。

  1. 首先,"文本串"与"模式串"头部对齐,从尾部开始比较。"S"与"E"不匹配。这时,"S"就被称为"坏字符"(bad character),即不匹配的字符,它对应着模式串的第6位。且"S"不包含在模式串"EXAMPLE"之中(相当于最右出现位置是-1),这意味着可以把模式串后移6-(-1)=7位,从而直接移到"S"的后一位。
  2. 依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,"P"包含在模式串"EXAMPLE"之中。因为“P”这个“坏字符”对应着模式串的第6位(从0开始编号),且在模式串中的最右出现位置为4,所以,将模式串后移6-4=2位,两个"P"对齐。
  3. 依次比较,得到 “MPLE”匹配,称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好后缀。
  4. 发现“I”与“A”不匹配:“I”是坏字符。如果是根据坏字符规则,此时模式串应该后移2-(-1)=3位。问题是,有没有更优的移法?
  1. 更优的移法是利用好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串中上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。所有的“好后缀”(MPLE、PLE、LE、E)之中,只有“E”在“EXAMPLE”的头部出现,所以后移6-0=6位。可以看出,“坏字符规则”只能移3位,“好后缀规则”可以移6位。每次后移这两个规则之中的较大值。这两个规则的移动位数,只与模式串有关,与原文本串无关。

  2. 继续从尾部开始比较,“P”与“E”不匹配,因此“P”是“坏字符”,根据“坏字符规则”,后移 6 - 4 = 2位。因为是最后一位就失配,尚未获得好后缀。
(彩蛋)BMH算法——省去好后缀表的优化算法

BMH它不再像BM算法一样关注失配的字符(好后缀),它的关注的焦点在于这个坏字符上,根据这个字符在模式串P中出现的最后的位置算出偏移长度,否则偏移模式串的长度。
研究表明,坏字符表在匹配过程中占主导作用的概率为94.03%(其实脑子里想想就知道了),远高于好后缀表的使用。同时相对来说好后缀表的计算过程相对复杂。BMH算法从减少比较次数来提高BM算法的性能。

  1. 字符X不在模板P中,则跳跃的步数为模板P的长度
  2. 字符X在模板P中,跳跃的步数为字符X距离离尾部最近的字符X的距离(不包括最后一个字符)
const int maxNum = 1005;
int shift[maxNum];
int BMH(const string& T, const string& P) {
    int n = T.length();
    int m = P.length();

    // 默认值,主串左移m位
    for(int i = 0; i < maxNum; i++) {
        shift[i] = m;
    }

    // 模式串P中每个字母出现的最后的下标,最后一个字母除外
    // 主串从不匹配最后一个字符,所需要左移的位数
    for(int i = 0; i < m - 1; i++) {
        shift[P[i]] = m - i - 1;
    }

    // 模式串开始位置在主串的哪里
    int s = 0;
    // 从后往前匹配的变量
    int j;
    while(s <= n - m) {
        j = m - 1;
        // 从模式串尾部开始匹配
        while(T[s + j] == P[j]) {
            j--;
            // 匹配成功
            if(j < 0) {
                return s;
            }
        }
        // 找到坏字符(当前跟模式串匹配的最后一个字符)
        // 在模式串中出现最后的位置(最后一位除外)
        // 所需要从模式串末尾移动到该位置的步数
        s = s + shift[T[s + m - 1]];
    }
    return -1;
}

6. Sunday算法(直接复制过来的)

上文中,我们已经介绍了KMP算法和BM算法,这两个算法在最坏情况下均具有线性的查找时间。但实际上,KMP算法并不比最简单的c库函数strstr()快多少,而BM算法虽然通常比KMP算法快,但BM算法也还不是现有字符串查找算法中最快的算法,本文最后再介绍一种比BM算法更快的查找算法即Sunday算法。

Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很相似:

只不过Sunday算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。
如果该字符没有在模式串中出现则直接跳过,即移动位数 = 匹配串长度 + 1;
否则,其移动位数 = 模式串中最右端的该字符到末尾的距离+1。
下面举个例子说明下Sunday算法。假定现在要在文本串"substring searching algorithm"中查找模式串"search"。

  1. 刚开始时,把模式串与文本串左边对齐:
    s u b s t r ing searching algorithm
    s e a r c h
  2. 结果发现在第2个字符处发现不匹配,不匹配时关注文本串中参加匹配的最末位字符的下一位字符,即标粗的字符 i,因为模式串search中并不存在i,所以模式串直接跳过一大片,向右移动位数 = 匹配串长度 + 1 = 6 + 1 = 7,从 i 之后的那个字符(即字符n)开始下一步的匹配,如下图:

substri n g _ s e a rching algorithm
    s e a r c h

  1. 结果第一个字符就不匹配,再看文本串中参加匹配的最末位字符的下一位字符,是'r',它出现在模式串中的倒数第3位,于是把模式串向右移动3位(r 到模式串末尾的距离 + 1 = 2 + 1 =3),使两个'r'对齐,如下:
    substring s e a r c h ing algorithm
         s e a r c h

  2. 匹配成功。

回顾整个过程,我们只移动了两次模式串就找到了匹配位置,缘于Sunday算法每一步的移动量都比较大,效率很高。完。

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

推荐阅读更多精彩内容

  • 数据结构与算法--KMP算法查找子字符串 部分内容和图片来自这三篇文章: 这篇文章、这篇文章、还有这篇他们写得非常...
    sunhaiyu阅读 1,722评论 1 21
  • 引言 字符串匹配一直是计算机科学领域研究和应用的热门领域,算法的改进研究一直是一个十分困难的课题。作为字符串匹配中...
    潮汐行者阅读 1,624评论 2 6
  • 数据结构 第8讲 KMP算法 讲这个算法之前,我们首先了解几个概念: 串:又称字符串,是由零个或多个字符组成的有限...
    rainchxy阅读 1,266评论 0 3
  • 文章大纲:1.KMP算法概念2.KMP算法中最核心的next[] 数组是如何生成的3.使用KMP算法 匹配字符串 ...
    柠檬乌冬面阅读 806评论 0 3
  • 实现的功能: 在需要print的时候,利用自定义Log输出当前print语句所在文件及代码所在行数. #if DE...
    红茶绅士阅读 293评论 0 0