‘壹’ 图解KMP字符串匹配算法
kmp算法跟之前讲的bm算法思想有一定的相似性。之前提到过,bm算法中有个好后缀的概念,而在kmp中有个好前缀的概念,什么是好前缀,我们先来看下面这个例子。
观察上面这个例子,已经匹配的abcde称为好前缀,a与之后的bcde都不匹配,所以没有必要再比一次,直接滑动到e之后即可。
那如果前缀中有互相匹配的字符呢?
观察上面这个例子,这个时候如果我们直接滑到好前缀之后,则会过度滑动,错失匹配子串。那我们如何根据好前缀来进行合理滑动?
其实就是看当前的好前缀的前缀和后缀是否有匹配的,找到最长匹配长度,直接滑动。鉴于不止一次找最长匹配长度,我们完全可以先初始化一个数组,保存在当前好前缀情况下,最长匹配长度是多少,这时候我们的next数组就出来了。
我们定义一个next数组,表示在当前好前缀下,好前缀的前缀和后缀的最长匹配子串长度,这个最长匹配长度表示这个子串之前已经匹配过匹配了,不需要再次进行匹配,直接从子串的下一个字符开始匹配。
我们是否每次算next[i]时都需要每一个字符进行匹配,是否可以根据next[i - 1]进行推导以便减少不必要的比较。
带着这个思路我们来看看下面的步骤:
假设next[i - 1] = k - 1;
如果modelStr[k] = modelStr[i] 则next[i]=k
如果modelStr[k] != modelStr[i],我们是否可以直接认定next[i] = next[i - 1]?
通过上面这个例子,我们可以很清晰地看到,next[i]!=next[i-1],那当modelStr[k]!=modelStr[i]时候,我们已知next[0],next[1]…next[i-1],如何推导出next[i]呢?
假设modelStr[x…i]是前缀后缀能匹配的最长后缀子串,那么最长匹配前缀子串为modelStr[0…i-x]
我们在求这个最长匹配串的时候,他的前面的次长匹配串(不包含当前i的),也就是modelStr[x…i-1]在之前应该是已经求解出来了的,因此我们只需要找到这个某一个已经求解的匹配串,假设前缀子串为modelStr[0…i-x-1],后缀子串为modelStr[x…i-1],且modelStr[i-x] == modelStr[i],这个前缀后缀子串即为次前缀子串,加上当前字符即为最长匹配前缀后缀子串。
代码实现
首先在kmp算法中最主要的next数组,这个数组标志着截止到当前下标的最长前缀后缀匹配子串字符个数,kmp算法里面,如果某个前缀是好前缀,即与模式串前缀匹配,我们就可以利用一定的技巧不止向前滑动一个字符,具体看前面的讲解。我们提前不知道哪些是好前缀,并且匹配过程不止一次,因此我们在最开始调用一个初始化方法,初始化next数组。
1.如果上一个字符的最长前缀子串的下一个字符==当前字符,上一个字符的最长前缀子串直接加上当前字符即可
2.如果不等于,需要找到之前存在的最长前缀子串的下一个字符等于当前子串的,然后设置当前字符子串的最长前缀后缀子串
然后开始利用next数组进行匹配,从第一个字符开始匹配进行匹配,找到第一个不匹配的字符,这时候之前的都是匹配的,接下来先判断是否已经是完全匹配,是直接返回,不是,判断是否第一个就不匹配,是直接往后面匹配。如果有好前缀,这时候就利用到了next数组,通过next数组知道当前可以从哪个开始匹配,之前的都不用进行匹配。
传统的字符串模式匹配算法(也就是BF算法)就是对于主串和模式串双双自左向右,一个一个字符比较,如果不匹配,主串和模式串的位置指针都要回溯。这样的算法时间复杂度为O(n*m),其中n和m分别为串s和串t的长度。
KMP 算法是由Knuth,Morris和Pratt等人共同提出的,所以成为Knuth-Morris-Pratt算法,简称KMP算法。KMP算法是字符串模式匹配中的经典算法。和BF算法相比,KMP算法的不同点是匹配过程中,主串的位置指针不会回溯,这样的结果使得算法时间复杂度只为O(n+m)。
‘叁’ 字符串匹配算法的使用(未完待整理)
字符串的匹配在Java中都知道使用indexOf函数来实现,那么其匹配算法是怎么样的呢?
单模式和多模式的区别就是一次遍历主串能否将多个模式的字符串都查找出来。
英文全称为Brute Force,暴力匹配算法,匹配字符串的方法比较暴力,也比较简单易懂。其大概的思路就是:
我们可以看到,在极端情况下,在主串 aaaa...aab 中寻找模式串 aab ,那么总共需要寻找(n-m+1)次,且每次都需要比对m次,那么时间复杂度将是 (n-m+1)*m ,即 O(n*m) ;但实际上并不会这么低效,因为我们的使用场景中主串和模式串都不会太长,而且在每个子串和模式串进行比对时,只要中途有一个不匹配,那么当前比对就会提前结束,因此大部分情况下,时间复杂度都会比 O(n*m) 要好。
我们在BF算法的基础上引入哈希算法,我们不需要将每个子串与模式串逐个字符地进行比较,而是计算得出每个子串的hash值,然后和模式串的hash值进行比较,如果有相等的,那就说明有子串和模式串匹配上了。
虽然我们只需要比对模式串和子串的hash值就能得到匹配结果,次数为(n-m+1),但是对每个子串进行hash计算的时候,是要遍历每个字符的,因此次数也是m,那么总的时间复杂度还是 O(n*m) ,并没有明显地提升。
那么我们该如何想出一个办法,使得每个子串hash值的计算时间得到提升呢?这就是RK算法的精髓,假设子串包含的字符集中元素个数为k,那么就用k进制数来代表这个子串,然后hash的过程就是将这个k进制的数转换为十进制的数,这个十进制的数就是该子串的hash值。
相邻子串的hash值计算是有规律的,我们只需要遍历一次主串就能得到所有子串的hash值,算法复杂度为O(n),而不是像原先一样,每个子串都需要O(m)的时间复杂度。
然后将模式串的hash值和所有子串的hash值进行比较,每次比较的时间复杂度是 O(1) ,总共比较(n-m+1)次,所以RK算法的总的时间开销为 O(n)+O(1)*O(n-m+1) ,即为 O(n) ,时间复杂度比BF算法更加高效。
当然,有hash的地方就有可能会存在hash冲突,有可能子串和hash值和模式串的hash值是一样的,但内容就是不一样,此时怎么办呢?其实很简单,对于hash值一样的子串,我们增加双保险,再比较一下这m个字符是否都一样即可,总的时间开销为 O(n)+O(1)*O(n-m+1)+O(m) ,即为 O(n) 。
如果极端情况下出现了很多hash冲突呢?我们对于每个和模式串相同hash值的子串都需要逐一再进行比较,那么总的时间开销就会为 O(n)+O(1)*O(n-m+1)+O(m)*O(n-m+1) ,即为 O(n*m) ,不过这种概率太小了,大部分情况下都不会这样。
在真正的文本编辑器中查找和替换某个字符串时,使用的算法既不是上述的BF算法,也不是RK算法;BF算法只适合不是很长的主串,RK算法则要设计一个冲突概率很低的hash算法,这个比较困难,所以实际使用的是BM算法,它是工程中非常常用的一种字符串匹配算法,效率也是最高的。
算法的思想和过程有些复杂,待以后整理。
KMP算法在本质上是和BM算法一样的。算法的思想和过程有些复杂,待以后整理。
浏览器输入框中的智能输入匹配是怎么实现的,它是怎么做动态字符串匹配查找的呢?这就用到了Trie树。
又名字典树,是一种专门用来快速查找字符串前缀匹配结果的树形结构,其本质就是将所有字符串的重复的前缀合并在一起,构造一个多叉树。
其中,根节点不包含任何信息,每个节点表示一个字符,从根节点到红色节点的一条路径表示存储的一个字符串。当我们在如上Trie树中查找"he"时,发现"he"并非是一个字符串,而是"hello"和"her"的公共前缀,那么就会找到这两个字符串返回。
Trie树在内存中是如何存储的呢?因为每一个节点都可能是包含所有字符的,所以每一个节点都是一个数组(或者散列表),用来存储每个字符及其后缀节点的指针。
使用Trie树,最开始构建的时候,时间复杂度为 O(n) ,其中n为所有字符串长度之和,但是一旦构建完成,频繁地查询某个字符串是非常高效的,时间复杂度为 O(k) ,其中k为查找字符串的长度。
Trie树虽然查询效率很高,但是比较浪费内存,每一个节点都必须维护一个数组存放所有可能的字符数据及其指向下一个节点的指针,因此在所有字符串公共前缀并不多的时候,内存空间浪费地就更多了。这种问题其实也有对应的解决办法,我们可以不使用数组,而是使用有序数组、散列表、红黑树来存放,可以相应地降低性能来节省内存空间。
Trie树除了可以实现浏览器动态输入内容查找候选项的功能外,还可以实现多模式地敏感词匹配功能。假设我们需要对用户输入的内容进行敏感词检查,将所有的敏感内容用***代替,那么该如何实现呢?
首先我们可以维护一个敏感词字典,使用上述四种单模式匹配算法也可以实现,但是需要遍历N次用户输入的内容,其中N是所有敏感词的模式串,显得非常低效。但是我们如果将敏感词字典维护为一个Trie树,然后将用户输入的内容从位置0开始在Trie树中进行查询,如果匹配到红色节点,那么说明有敏感词;如果没有匹配到红色节点,就从用户输入内容的下一个位置开始继续在Trie树中查询,直至将用户输入内容遍历完,因此我们只是遍历了一遍主串。
然而更高效的多模式字符串匹配使用地更多的是如下的AC自动机。
如果把Trie树比作BF算法,KMP算法是BF算法的改进,那么AC自动机就是利用同样的思想改进了Trie树。
算法的思想和过程有些复杂,待以后整理。
‘肆’ 串的应用kmp算法。求一个字符串在另一个字符串中第一次出现的位置。
KMP.java
源代码为:
package algorithm.kmp;
/**
* KMP算法的Java实现例子与测试、分析
* @author 崔卫兵
* @date 2009-3-25
*/
public class KMP {
/**
* 对子串加以预处理,从而找到匹配失败时子串回退的位置
* 找到匹配失败时的最合适的回退位置,而不是回退到子串的第一个字符,即可提高查找的效率
* 因此为了找到这个合适的位置,先对子串预处理,从而得到一个回退位置的数组
* @param B,待查找子串的char数组
* @return
*/
public static int[] preProcess(char [] B) {
int size = B.length;
int[] P = new int[size];
P[0]=0;
int j=0;
//每循环一次,就会找到一个回退位置
for(int i=1;i<size;i++){
//当找到第一个匹配的字符时,即j>0时才会执行这个循环
//或者说p2中的j++会在p1之前执行(限于第一次执行的条件下)
//p1
while(j>0 && B[j]!=B[i]){
j=P[j];
}
//p2,由此可以看出,只有当子串中含有重复字符时,回退的位置才会被优化
if(B[j]==B[i]){
j++;
}
//找到一个回退位置j,把其放入P[i]中
P[i]=j;
}
return P;
}
/**
* KMP实现
* @param parStr
* @param subStr
* @return
*/
public static void kmp(String parStr, String subStr) {
int subSize = subStr.length();
int parSize = parStr.length();
char[] B = subStr.toCharArray();
char[] A = parStr.toCharArray();
int[] P = preProcess(B);
int j=0;
int k =0;
for(int i=0;i<parSize;i++){
//当找到第一个匹配的字符时,即j>0时才会执行这个循环
//或者说p2中的j++会在p1之前执行(限于第一次执行的条件下)
//p1
while(j>0 && B[j]!=A[i]){
//找到合适的回退位置
j=P[j-1];
}
//p2 找到一个匹配的字符
if(B[j]==A[i]){
j++;
}
//输出匹配结果,并且让比较继续下去
if(j==subSize){
j=P[j-1];
k++;
System.out.printf("Find subString '%s' at %d\n",subStr,i-subSize+1);
}
}
System.out.printf("Totally found %d times for '%s'.\n\n",k,subStr);
}
public static void main(String[] args) {
//回退位置数组为P[0, 0, 0, 0, 0, 0]
kmp("abcdeg, abcdeh, abcdef!这个会匹配1次","abcdef");
//回退位置数组为P[0, 0, 1, 2, 3, 4]
kmp("Test ititi ititit! Test ititit!这个会匹配2次","ititit");
//回退位置数组为P[0, 0, 0]
kmp("测试汉字的匹配,崔卫兵。这个会匹配1次","崔卫兵");
//回退位置数组为P[0, 0, 0, 1, 2, 3, 4, 5, 6]
kmp("这个会匹配0次","it1it1it1");
}
}
‘伍’ KMP模式匹配算法是什么
KMP模式匹配算法是一种改进算法,是由D.E.Knuth、J.H.Morris和v.R.Pratt提出来的,因此人们称它为“克努特-莫里斯-普拉特操作”,简称KMP算法。此算法可以在O(n+m)的时间数量级上完成串的模式匹配操作。其改进在于:每当一趟匹配过程出现字符不相等时,主串指针i不用回溯,而是利用已经得到的“部分匹配”结果,将模式串的指针j向右“滑动”尽可能远的一段距离后,继续进行比较。
1.KMP模式匹配算法分析回顾图4-5所示的匹配过程示例,在第三趟匹配中,当i=7、j=5字符比较不等时,又从i=4、j=1重新开始比较。然而,经仔细观察发现,i=4和j=1、i=5和j=1以及i=6和j=1这三次比较都是不必进行的。因为从第三趟部分匹配的结果就可得出,主串中的第4、5和6个字符必然是b、c和a(即模式串第2、第2和第4个字符)。因为模式中的第一个字符是a,因此它无须再和这三个字符进行比较,而仅需将模式向右滑动2个字符的位置进行i=7、j=2时的字符比较即可。同理,在第一趟匹配中出现字符不等时,仅需将模式串向右移动两个字符的位置继续进行i=2、j=1时的字符比较。由此,在整个匹配过程中,i指针没有回溯,如图1所示。
图1改进算法的模式匹配过程示意
‘陆’ 【算法笔记】字符串匹配
BF 算法中的 BF 是 Brute Force 的缩写,中文叫作暴力匹配算法,也叫朴素匹配算法:
主串和模式串:
在字符串 A 中查找字符串 B,那字符串 A 就是主串,字符串 B 就是模式串。我们把主串的长度记作 n,模式串的长度记作 m
我们在主串中,检查起始位置分别是 0、1、2…n-m 且长度为 m 的 n-m+1 个子串,看有没有跟模式串匹配的。
BF 算法的时间复杂度是 O(n*m)
等价于
比如匹配Google 和Goo 是最好时间复杂度,匹配Google 和ble是匹配失败的最好时间复杂度。
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth与J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特—莫里斯—普拉特算法。KMP算法主要分为两个步骤:字符串的自我匹配,目标串和模式串之间的匹配。
看来网上很多的文章,感觉很多的都没有说清楚,这里直接复制阮一峰的内容,讲的很清晰
内容来自 http://www.ruanyifeng.com/blog/
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
因为B与A不匹配,搜索词再往后移。
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
接着比较字符串和搜索词的下一个字符,还是相同。
直到字符串有一个字符,与搜索词对应的字符不相同为止。
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。
怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。
已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:
因为 6 - 2 等于4,所以将搜索词向后移动4位。
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。
因为空格与A不匹配,继续后移一位。
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。
BM(Boyer-Moore)算法。它是一种非常高效的字符串匹配算法,有实验统计,它的性能是着名的KMP 算法的 3 到 4 倍。
BM 算法包含两部分,分别是坏字符规则(bad character rule)和好后缀规则(good suffix shift)
未完待续
参考文章:
字符串匹配的Boyer-Moore算法
‘柒’ kmp算法详解
KMP模式匹配算法
KMP算法是一种改进的字符串匹配算法,其关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的明[4]。
求得模式的特征向量之后,基于特征分析的快速模式匹配算法(KMP模式匹配算法)与朴素匹配算法类似,只是在每次匹配过程中发生某次失配时,不再单纯地把模式后移一位,而是根据当前字符的特征数来决定模式右移的位数[3]。
include "string. h"
#include<assert. h>
int KMPStrMatching(String T, String P, int. N, int startIndex)
{int lastIndex=T.strlen() -P.strlen();
if((1 astIndex- startIndex)<0)//若 startIndex过大,则无法匹配成功
return (-1);//指向P内部字符的游标
int i;//指向T内部字符的游标
int j=0;//指向P内部字符的游标
for(i= startIndex; i <T.strlen(); i++)
{while(P[j]!=T[i]&& j>0)
j=N[j-1];
if(P[j]==T[i])
j++;
if(j ==P.strlen())
return(1-j+1);//匹配成功,返回该T子串的开始位置
}
return (-1);
}
‘捌’ 数据结构与算法——字符串匹配问题(KMP算法)
KMP算法也是比较着名的模式匹配算法。是由 D.E.Knuth,J.H.Morrs 和 VR.Pratt 发表的一个模式匹配算法。可以大大避免重复遍历的情况。
如果使用暴风算法的话,前面五个字母完全相等,直到第六个字母 "f" 和 "x" 不相等。如下图:
T = “abcdex”
j 123456
模式串 abcdex
next[j] 011111
T = "abcabx"
j 123456
模式串T abcabx
next[j] 011123
T = "ababaaaba"
j———————123456789
模式串T——— ababaaaba
next[j]————011234223
T = "aaaaaaaab"
j———————123456789
模式串T——— aaaaaaaab
next[j]————012345678
next数组其实就是求解字符串要回溯的位置
假设,主串S= “abcababca”;模式串T=“abcdex”,由以上分析得出next数组为011111,next数组意味着当主串与模式串不匹配时,都需要从第一个的位置重新比较。
KMP算法也是有缺陷的,比如主串S=“aaaabcde”,模式串T= “aaaaax”。next的数组就是012345;
当开始匹配时,当i= 5,j = 5时,我们发现字符"b"与字符“a”不相等,如上图,j = next[5] = 4;
由于T串的第二、三、四、五位置的字符都与首位“a”相等,那么可以用首位next[1]的值去取代与它相等的后续字符的next[j],那么next数组为{0,0,0,0,0,5};
在求解nextVal数组的5种情况