子字符串查找(2)——KMP算法

一、定义

KMP(Knuth-Morris-Pratt)算法,其实是对暴力查找算法的优化。
在暴力查找算法中,用于追踪文本的指针i每次都会回退到起始位置+1。事实上,当出现不匹配时,就能知晓一部分文本内容信息(模式字符串中当前匹配失败的字符前的所有字符)。

KMP算法在匹配失败时,总是将模式指针j设置为某个值以使文本指针i不用回退,所以关键是如何重置指针j的值,而这只与模式字符串本身有关,需要对模式字符串进行预处理,计算每个字串的最大公共前后缀长度

3-1 暴力查找算法与 KMP算法的对比

二、基本思想

2.1 KMP匹配示例

以模式字符串“ABCDABD”、文本“BBC ABCDAB ABCDABCDABDE”为例:

  1. 首先,文本“BBC ABCDAB ABCDABCDABDE”的第一个字符与模式字符串"ABCDABD"的第一个字符,进行比较。
    因为B与A不匹配,所以模式字符串后移1位,直到模式字符串有一个字符,与文本的第一个字符相同为止。
  1. 接着比较模式字符串和文本的下一个字符,还是相同。那么,继续后移,直到模式字符串中有一个字符与文本中对应的字符不相同为止。
  1. 当空格与D不匹配时,其实已经知道文本的前面6个匹配字符是"ABCDAB"。KMP算法的思想就是,设法利用这个已知信息,不要把"搜索位置i"移回已经比较过的位置,而是继续把它向后移,这样就提高了效率。
    那么如何做到这一点呢?可以针对模式字符串,算出一张《部分匹配表》(Partial Match Table),如下图(如何计算后面讲解):

按照下面的公式算出模式字符串向后移动的位数:
模式字符串的右移位数 = 已匹配的字符数 - 最后匹配字符的部分匹配值

  1. 上述图中,空格与D不匹配,模式字符串还要继续往后移。这时,“已匹配的字符数”=6("ABCDAB"),“最后匹配字符的部分匹配值”B=2。所以,移动位数 = 6 - 2 = 4,于是将模式字符串后移4位。
  1. 此时,空格与C不匹配,模式字符串还要继续往后移。这时,“已匹配的字符数”=2("AB"),“最后匹配字符的部分匹配值”B=0。所以,移动位数 = 2 - 0 = 2,于是将模式字符串后移2位。
  1. 此时,空格与A不匹配,直接后移1位。
  1. 此时,C与D不匹配,模式字符串还要继续往后移。这时,“已匹配的字符数”=6("ABCDAB"),“最后匹配字符的部分匹配值”B=2。所以,移动位数 = 6 - 2 = 4,于是将模式字符串后移4位。
  1. 逐位比较,直到模式字符串的最后一位,发现完全匹配,于是搜索完成。注:如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,重复上述步骤即可。

2.2 构造部分匹配表

上述示例中用到了一张部分匹配表(Partial Match Table)。那么如何构造呢?先来看两个概念:前缀后缀
前缀:指除了最后一个字符以外,一个字符串的全部头部组合。
后缀:指除了第一个字符以外,一个字符串的全部尾部组合。

例如,对于字符串“ABCDABD”:
前缀:A、AB、ABC、ABCD、ABCDA、ABCDAB
后缀:BCDABD、CDABD、DABD、ABD、BD、D

2.3 部分匹配表的本质

对于部分匹配表,所谓“部分匹配值”就是“前缀”和“后缀”的最长共有元素的长度

以“ABCDABD”为例:

字符串 前缀 后缀 最长共有元素的长度
A 0
AB A B 0
ABC A、AB C、BC 0
ABCD A、AB、ABC D、CD、BCD 0
ABCDA A、AB、ABC、ABCD ** A**、DA、CDA、BCDA 1
ABCDAB A、AB、ABC、ABCD、ABCDA B、AB、DAB、CDAB、BCDAB 2
ABCDABD A、AB、ABC、ABCD、ABCDA、ABCDAB D、BD、ABD、DABD、CDABD、BCDABD 0

再比如,"ABCDAB"之中有两个"AB",那么它的“部分匹配值”就是2("AB"的长度)。
模式字符串移动的时候,第一个"AB"向后移动4位(已匹配字符串长度6-已匹配字符串的最大公共前后缀2),就可以来到第二个"AB"的位置,如下图:


实际上,当出现失配时,并不需要真的去移动模式字符串。
上图中,失配位置j=6,移动后重新比较的位置j=2,我们可以发现j=2就是已匹配字符串“ABCDAB”的最大公共前后缀长度。
所以,可以专门用一个next[j]数组保存每个字符的回溯位置,有如下映射关系:

索引j 0 1 2 3 4 5 6
搜索词 A B C D A B D
部分匹配值 0 0 0 0 1 2 0
next[j] -1 0 0 0 0 1 2

注:可以观察到 next 序列其实就是部分匹配值整体右移1位,next[0]记 -1,next[1]记 0

其本质就是一个确定有限状态机(DFA):

2.4 构造next[]数组

假设模式字符串为P0P1P2…Pn ,用一个next[]数组记录模式字符串的部分匹配映射值。
next[j]=k表示:子串P[0~j-1]的最大公共前后缀长度为k。
初始时,记next[0]=-1next[1]=0,且假设已知next[j]=k。则转化为一个数学归纳问题:
①初始:next[0]=-1next[1]=0
②已知:next[j]=k
③求解next[j+1]

求解过程:
next[j]=k说明P[0]~P[k-1]与 P[j-k]~P[j-1]依次相同。

  1. 如果P[k] = P[j],那么next[j+1] = k+1,即子串P[0]~P[j]的最大公共前后缀长度为k+1
  2. 如果P[k] ≠ P[j],那么需要将P[0]~P[k]右移(参见2.3 部分匹配表的本质),右移多少位呢?
    j'=next[k]next[k]表示子串P[0]~P[k-1]的最大公共前后缀长度),j'即右移位数。然后重复上述步骤,继续比较`P[j']=P[j]。

2.5 构造next[]数组的实现源码

构造 next[]数组:

/**
 * 根据模式字符串,生成next数组
 */
public int[] makeNext(String ptn) {
    if (ptn == null)
        throw new IllegalArgumentException("ptn");
 
    int n = ptn.length();
    int[] next = new int[n];
 
    next[0] = -1;
    int j = 0; // 指针j跟踪当前待求解的模式字符
    int k = next[j]; // 假设已知next[j]==k
 
    while (j < n - 1)// 模式字符串的next[0]已知,故剩余n-1个待求解字符
    {
        if (k == -1 || ptn.charAt(j) == ptn.charAt(k)) {
            next[j + 1] = k + 1;
            k++;
            j++;
        } else {
            k = next[k];
        }
    }
    return next;
}

三、KMP算法实现

public int KMP(String ptn, String txt) {
    int[] next = makeNext(ptn);
 
    int i = 0, j = 0;         // i指示文本,j指示模式字符串
    while (i < txt.length() && j < ptn.length()) {
        if (next[j] == -1 || ptn.charAt(j) == txt.charAt(i)) {
            i++;
            j++;
        } else {
            j = next[j];    // 模式字符串右移
        }
    }
    if (j == ptn.length())    // 匹配成功
        return i - j;
    else                      // 匹配失败
        return -1;
}

四、性能分析

实际应用中,KMP算法比暴力算法的优势并不十分明显,因为极少有应用程序需要在重复性很高的文本中查找重复性很高的模式。
但是,KMP算法的优势在于文本指针不必回退,这使得它可以很方便得处理长度不确定的输入流(如标准输入流)。

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

推荐阅读更多精彩内容