博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[转]KMP算法
阅读量:5162 次
发布时间:2019-06-13

本文共 7274 字,大约阅读时间需要 24 分钟。

网上有两篇比较经典的关于KMP的讲述,

http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

http://www.cnblogs.com/yjiyjige/p/3263858.html

1.当不匹配时,i表示t中当前查找位置,j表示p中当前查找位置。阮做法是把i向右偏移k1个位置,j重置为0,而孤影的做法是i保持不变,j向左偏移k2个位置。本质上是一样的,关键还是偏移值k的计算

2.KMP算法的核心是,对于p中的每一个位置如果不匹配时,都会有一个对应的偏移量k。所以一般都会有一个和p长度对应的数组,存放每个位置的偏移量。

3.孤影中j为什么偏移k个位置的原因讲解很赞,在计算k的过程中也有理有据,而阮的做法更偏向于结果导向性,只是说为什么要这么做,没有给出证明,但这种做饭倒是很容易理解容易掌握。

 

 

 

两篇原文摘录如下:

字符串匹配的KMP算法

 

作者: 

日期: 

是计算机的基本任务之一。

举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?

许多算法可以完成这个任务,(简称KMP)是最常用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。

这种算法不太容易理解,网上有很多,但读起来都很费劲。直到读到的文章,我才真正理解这种算法。下面,我用自己的语言,试图写一篇比较好懂的KMP算法解释。

1.

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

2.

因为B与A不匹配,搜索词再往后移。

3.

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

4.

接着比较字符串和搜索词的下一个字符,还是相同。

5.

直到字符串有一个字符,与搜索词对应的字符不相同为止。

6.

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

7.

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

8.

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

9.

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

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

因为 6 - 2 等于4,所以将搜索词向后移动4位。

10.

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

11.

因为空格与A不匹配,继续后移一位。

12.

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

13.

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

14.

下面介绍《部分匹配表》是如何产生的。

首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

15.

"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

  - "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

16.

"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

(完)

 

 

 

KMP算法应该是每一本《数据结构》书都会讲的,算是知名度最高的算法之一了,但很可惜,我大二那年压根就没看懂过~~~

之后也在很多地方也都经常看到讲解KMP算法的文章,看久了好像也知道是怎么一回事,但总感觉有些地方自己还是没有完全懂明白。这两天花了点时间总结一下,有点小体会,我希望可以通过我自己的语言来把这个算法的一些细节梳理清楚,也算是考验一下自己有真正理解这个算法。

 

什么是KMP算法:

KMP是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的。其中第一位就是《计算机程序设计艺术》的作者!!

KMP算法要解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。说简单点就是我们平时常说的关键字搜索。模式串就是关键字(接下来称它为P),如果它在一个主串(接下来称为T)中出现,就返回它的具体位置,否则返回-1(常用手段)。

 

首先,对于这个问题有一个很单纯的想法:从左到右一个个匹配,如果这个过程中有某个字符不匹配,就跳回去,将模式串向右移动一位。这有什么难的?

我们可以这样初始化:

 

之后我们只需要比较i指针指向的字符和j指针指向的字符是否一致。如果一致就都向后移动,如果不一致,如下图:

 

 

A和E不相等,那就把i指针移回第1位(假设下标从0开始),j移动到模式串的第0位,然后又重新开始这个步骤:

 

基于这个想法我们可以得到以下的程序:

1 /** 2  3  * 暴力破解法 4  5  * @param ts 主串 6  7  * @param ps 模式串 8  9  * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-110 11  */12 13 public static int bf(String ts, String ps) {14 15     char[] t = ts.toCharArray();16 17     char[] p = ps.toCharArray();18 19     int i = 0; // 主串的位置20 21     int j = 0; // 模式串的位置22 23     while (i < t.length && j < p.length) {24 25        if (t[i] == p[j]) { // 当两个字符相同,就比较下一个26 27            i++;28 29            j++;30 31        } else {32 33            i = i - j + 1; // 一旦不匹配,i后退34 35            j = 0; // j归036 37        }38 39     }40 41     if (j == p.length) {42 43        return i - j;44 45     } else {46 47        return -1;48 49     }50 51 }

上面的程序是没有问题的,但不够好!(想起我高中时候数字老师的一句话:我不能说你错,只能说你不对~~~)

如果是人为来寻找的话,肯定不会再把i移动回第1位,因为主串匹配失败的位置前面除了第一个A之外再也没有A,我们为什么能知道主串前面只有一个A?因为我们已经知道前面三个字符都是匹配的!(这很重要)。移动过去肯定也是不匹配的!有一个想法,i可以不动,我们只需要移动j即可,如下图:

 

上面的这种情况还是比较理想的情况,我们最多也就多比较了再次。但假如是在主串“SSSSSSSSSSSSSA”中查找“SSSSB”,比较到最后一个才知道不匹配,然后i回溯,这个的效率是显然是最低的。

 

大牛们是无法忍受“暴力破解”这种低效的手段的,于是他们三个研究出了KMP算法。其思想就如同我们上边所看到的一样:“利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改j指针,让模式串尽量地移动到有效的位置。”

 

所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道j指针要移动到哪

 

接下来我们自己来发现j的移动规律:

 

如图:C和D不匹配了,我们要把j移动到哪?显然是第1位。为什么?因为前面有一个A相同啊:

 

如下图也是一样的情况:

 

可以把j指针移动到第2位,因为前面有两个字母是一样的:

 

至此我们可以大概看出一点端倪,当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的

如果用数学公式来表示是这样的

P[0 ~ k-1] == P[j-k ~ j-1]

这个相当重要,如果觉得不好记的话,可以通过下图来理解:

 

弄明白了这个就应该可能明白为什么可以直接将j移动到k位置了。

因为:

当T[i] != P[j]时

有T[i-j ~ i-1] == P[0 ~ j-1]

由P[0 ~ k-1] == P[j-k ~ j-1]

必然:T[i-k ~ i-1] == P[0 ~ k-1]

公式很无聊,能看明白就行了,不需要记住。

这一段只是为了证明我们为什么可以直接将j移动到k而无须再比较前面的k个字符。

 

好,接下来就是重点了,怎么求这个(这些)k呢?因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置。

 

很多教材或博文在这个地方都是讲得比较含糊或是根本就一笔带过,甚至就是贴一段代码上来,为什么是这样求?怎么可以这样求?根本就没有说清楚。而这里恰恰是整个算法最关键的地方。

1 public static int[] getNext(String ps) { 2  3     char[] p = ps.toCharArray(); 4  5     int[] next = new int[p.length]; 6  7     next[0] = -1; 8  9     int j = 0;10 11     int k = -1;12 13     while (j < p.length - 1) {14 15        if (k == -1 || p[j] == p[k]) {16 17            next[++j] = ++k;18 19        } else {20 21            k = next[k];22 23        }24 25     }26 27     return next;28 29 }

这个版本的求next数组的算法应该是流传最广泛的,代码是很简洁。可是真的很让人摸不到头脑,它这样计算的依据到底是什么?

好,先把这个放一边,我们自己来推导思路,现在要始终记住一点,next[j]的值(也就是k)表示,当P[j] != T[i]时,j指针的下一步移动位置

先来看第一个:当j为0时,如果这时候不匹配,怎么办?

 

像上图这种情况,j已经在最左边了,不可能再移动了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。

如果是当j为1的时候呢?

 

显然,j指针一定是后移到0位置的。因为它前面也就只有这一个位置了~~~

 

下面这个是最重要的,请看如下图:

  

 

请仔细对比这两个图。

我们发现一个规律:

当P[k] == P[j]时,

有next[j+1] == next[j] + 1

其实这个是可以证明的:

因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)

这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。

即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。

这里的公式不是很好懂,还是看图会容易理解些。

 

那如果P[k] != P[j]呢?比如下图所示:

 

像这种情况,如果你从代码上看应该是这一句:k = next[k];为什么是这样子?你看下面应该就明白了。

 

现在你应该知道为什么要k = next[k]了吧!像上边的例子,我们已经不可能找到[ A,B,A,B ]这个最长的后缀串了,但我们还是可能找到[ A,B ]、[ B ]这样的前缀串的。所以这个过程像不像在定位[ A,B,A,C ]这个串,当C和主串不一样了(也就是k位置不一样了),那当然是把指针移动到next[k]啦。

 

有了next数组之后就一切好办了,我们可以动手写KMP算法了:

1 public static int KMP(String ts, String ps) { 2  3     char[] t = ts.toCharArray(); 4  5     char[] p = ps.toCharArray(); 6  7     int i = 0; // 主串的位置 8  9     int j = 0; // 模式串的位置10 11     int[] next = getNext(ps);12 13     while (i < t.length && j < p.length) {14 15        if (j == -1 || t[i] == p[j]) { // 当j为-1时,要移动的是i,当然j也要归016 17            i++;18 19            j++;20 21        } else {22 23            // i不需要回溯了24 25            // i = i - j + 1;26 27            j = next[j]; // j回到指定位置28 29        }30 31     }32 33     if (j == p.length) {34 35        return i - j;36 37     } else {38 39        return -1;40 41     }42 43 }

和暴力破解相比,就改动了4个地方。其中最主要的一点就是,i不需要回溯了。

 

最后,来看一下上边的算法存在的缺陷。来看第一个例子:

 

显然,当我们上边的算法得到的next数组应该是[ -1,0,0,1 ]

所以下一步我们应该是把j移动到第1个元素咯:

 

不难发现,这一步是完全没有意义的。因为后面的B已经不匹配了,那前面的B也一定是不匹配的,同样的情况其实还发生在第2个元素A上。

显然,发生问题的原因在于P[j] == P[next[j]]

所以我们也只需要添加一个判断条件即可:

public static int[] getNext(String ps) {    char[] p = ps.toCharArray();    int[] next = new int[p.length];    next[0] = -1;    int j = 0;    int k = -1;    while (j < p.length - 1) {       if (k == -1 || p[j] == p[k]) {           if (p[++j] == p[++k]) { // 当两个字符相等时要跳过              next[j] = next[k];           } else {              next[j] = k;           }       } else {           k = next[k];       }    }    return next;} 

好了,至此。KMP算法也结束了。

很奇怪,好像不是很难的东西怎么就把我困住这么久呢?

仔细想想还是因为自己太浮躁了,以前总是草草应付,很多细节都没弄清楚,就以为自己懂了。结果就只能是似懂非懂的。要学东西真的需要静下心来。

 

转载于:https://www.cnblogs.com/tianlanliao/p/3673791.html

你可能感兴趣的文章
AtCoder Regular Contest 100 E - Or Plus Max
查看>>
08CMS Variable Override Write Arbitrarily WEBSHELL Into Arbitrarily Path
查看>>
调用UEditor的图片上传组件进行图片上传,并解决多编辑器共存的取值问题
查看>>
修改jquery.automeplete,使其支持value匹配
查看>>
linux curl的使用
查看>>
linux运维、架构之路-Hadoop完全分布式集群搭建
查看>>
redis学习系列——redis主从同步配置及原理介绍
查看>>
titanium开发教程-02-11增加交互性,在任何view
查看>>
Microsoft Visual Studio 2012正式版官方下载
查看>>
Unity3d 小游戏从入门到???
查看>>
关于API设计规范
查看>>
mongoDB安装使用
查看>>
vue.js学习文档
查看>>
秒杀多线程第六篇 经典线程同步 事件Event
查看>>
冬日随想
查看>>
MySQL 5.6 my.conf参数说明
查看>>
【WPF】OpacityMask作用于Button的一点体会
查看>>
ant构建工具
查看>>
Alpha项目冲刺(团队作业5)
查看>>
codeforce830A. Office Keys
查看>>