史上最好理解的KMP算法
2017-03-13 22:08
211 查看
KMP算法的解释很多,但是大多都晦涩难懂。
我看了好几篇,决定把其中讲得比较好的几篇归纳一下。
但是版权属于它们:
The Knuth-Morris-Pratt Algorithm in my own words
字符串匹配的KMP算法
如果你看不懂KMP算法,那就看一看这篇文章
但是暴力匹配算法有一个问题:当你知道你的目标串和搜索串不匹配的时候,它已经附带了一些信息。
比如在如图匹配,实际上目标串已经匹配到了第七个字符D
如果接下来你还是把A往后移动一格,实际上你浪费了这次匹配。你应该把A往后移动四格才对。
而利用到了这个信息的就是KMP算法:
这个表你先用着,具体怎么生成接下来会说。
已知空格和D不匹配但是前面六格是匹配的。查表可知,对后一个匹配字符B对应的部分匹配值为2,根据如下公式
移动位数=已匹配的字符数-对应的部分匹配值
因为6-2=4,所以搜索词移动4位。
因为空格与c不匹配,搜索词还要继续往后移动。这时,已匹配的字符数为2(“AB”),对应的部分匹配值为0.所以,移动位数=2-0,结果为2,于是将搜索词后移两位。
依次类推。
而匹配串是怎么产生的呢?
“前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
产生依据:
- “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。
直观来看是两张图:
绿色是相同的字符串
为什么计算部分匹配表的时候不按套路出牌啊?为啥代码这么简单啊。
但是我们要知道,部分匹配表也是有信息可以利用的。
如图
当我们知道第j(此处为5)个字符的部分匹配值为i(此处为1)的时候,求第j+1(6)个字符的部分匹配值,我们只需要判断第i+1(2)个字符是不是和第j+1(6)是不是相等就可以了。如果相等,则第j+1(6)的部分匹配值必为i+1(2)。
所以代码有一行是:
但是如果这两个字符不相等呢?
从前面来找子前后缀
1 、如果要存在对称性,那么对称程度肯定比前面这个的对称程度小,所以要找个更小的对称,这个不用解释了吧,如果大那么就继承前面的对称性了。
2 、如果还是不相等,要找更小的对称,必然在对称内部还存在子对称,而且这个必须紧接着在子对称之后。
我看了好几篇,决定把其中讲得比较好的几篇归纳一下。
但是版权属于它们:
The Knuth-Morris-Pratt Algorithm in my own words
字符串匹配的KMP算法
如果你看不懂KMP算法,那就看一看这篇文章
暴力匹配算法
首先,我假设你已经是一个已经懂暴力匹配算法的人了。但是暴力匹配算法有一个问题:当你知道你的目标串和搜索串不匹配的时候,它已经附带了一些信息。
比如在如图匹配,实际上目标串已经匹配到了第七个字符D
如果接下来你还是把A往后移动一格,实际上你浪费了这次匹配。你应该把A往后移动四格才对。
而利用到了这个信息的就是KMP算法:
已知部分匹配表的KMP算法
首先我们先给出一个部分匹配表:这个表你先用着,具体怎么生成接下来会说。
Step1:
已知空格和D不匹配但是前面六格是匹配的。查表可知,对后一个匹配字符B对应的部分匹配值为2,根据如下公式
移动位数=已匹配的字符数-对应的部分匹配值
因为6-2=4,所以搜索词移动4位。
再来试一次:
因为空格与c不匹配,搜索词还要继续往后移动。这时,已匹配的字符数为2(“AB”),对应的部分匹配值为0.所以,移动位数=2-0,结果为2,于是将搜索词后移两位。
依次类推。
而匹配串是怎么产生的呢?
部分匹配表的产生
首先来了解前缀和后缀的概念“前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
产生依据:
- “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。
直观来看是两张图:
绿色是相同的字符串
java代码
但是看到求部分匹配表的java代码,我们又一次懵逼了。public int[] getNext(String b) { int len=b.length(); int j=0; int next[]=new int[len+1];//next表示长度为i的字符串前缀和后缀的最长公共部分,从1开始 next[0]=next[1]=0; for(int i=1;i<len;i++)//i表示字符串的下标,从0开始 {//j在每次循环开始都表示next[i]的值,同时也表示需要比较的下一个位置 while(j>0&&b.charAt(i)!=b.charAt(j))j=next[j]; if(b.charAt(i)==b.charAt(j))j++; next[i+1]=j; } return next; }
为什么计算部分匹配表的时候不按套路出牌啊?为啥代码这么简单啊。
但是我们要知道,部分匹配表也是有信息可以利用的。
如图
当我们知道第j(此处为5)个字符的部分匹配值为i(此处为1)的时候,求第j+1(6)个字符的部分匹配值,我们只需要判断第i+1(2)个字符是不是和第j+1(6)是不是相等就可以了。如果相等,则第j+1(6)的部分匹配值必为i+1(2)。
所以代码有一行是:
if(b.charAt(i)==b.charAt(j))j++; next[i+1]=j;
但是如果这两个字符不相等呢?
从前面来找子前后缀
1 、如果要存在对称性,那么对称程度肯定比前面这个的对称程度小,所以要找个更小的对称,这个不用解释了吧,如果大那么就继承前面的对称性了。
2 、如果还是不相等,要找更小的对称,必然在对称内部还存在子对称,而且这个必须紧接着在子对称之后。
while(j>0&&b.charAt(i)!=b.charAt(j))j=next[j];