KMP算法:在字符串s中搜索匹配match字符串,如果能找到返回首个匹配位置i,否则返回-1
提示:字符串匹配算法KMP算法,速度极快
之前咱们学过KMP算法必须要预备的预设数组信息:next数组
(1)KMP算法预备知识:字符串match的每一个位置i之前的字符串,前缀与后缀匹配的最大长度是多少?
这个文章你必须学会了,学透了,
才能理解今天的KMP算法,在一个字符串s中快速搜索match字符串,并返回匹配的首个字符位置i
题目
KMP算法:
在字符串s中搜索匹配查找match字符串,如果能找到,返回首个匹配位置i,否则返回-1
一、审题
s=abcf abcabck st
match=abcabck
显然在s中首个与match能匹配的位置是i=4
故返回s
如果match是akc啥的
不在s中能匹配到,那就返回-1即可
KMP算法干嘛的?
KMP算法,是寻找字符串str中match串所在的首个位置i的最优算法。
比如
s=abc abck t
match=abck
那么s中首个能与match 匹配的子串(匹配就是相等的意思)的起始位置i是?
python函数中,正则化匹配模块的re.search(s,match)函数,就是干这件事的
自然,暴力解 ,从s的0–N-1都去寻找一遍,每个位置i,都去从match的j=0位置开始重头匹配
这样做复杂度极其高o(n*m),n是s长度,m是match的长度
之后,如何加速这个匹配过程,那就要用到KMP算法!
今天咱专门讲KMP算法,
之前说过的KMP算法是需要准备前奏的:next预设信息数组
众所周知,算法时间复杂度和空间复杂度从来都是一对冤家,
你想速度快,不好意思,那代码就复杂,空间复杂度就很高
你想省空间,代码简单,那不好意思,你运行速度就很慢。
暴力解匹配字符串,不消耗额外空间,但是很慢
KMP为了加速到o(n),自然就避免不了消耗额外空间!
KMP需要准备一个很重要的预设信息数组next!
这个数组next是针对match串做的!只需要o(m)的速度,比o(n)小,可以忽略不计!
这个预设数组怎么定义的,代码怎么求?
看这个文章:
(1)KMP算法预备知识:字符串match的每一个位置i之前的字符串,前缀与后缀匹配的最大长度是多少?
你一定要把这个文章看透了!!!
KMP算法的预备知识:next预设信息数组
再说一遍next的含义:
next[i]代表什么,你多次反复理解,慢慢印象就很深刻了:
next数组啥意思呢?next[i]代表:match串的i位置前面,前缀与后缀相匹配的最大长度。
【1】即0–i-1范围上前后缀匹配的最大长度。
【2】next[i]还代表,0–i-1范围上第一个与i位置不匹配的位置。
这个含义理解了,咱们来设计代码快速根据0–i-1上的next信息,推导出next[i]来。
代码流程如下:
(1)咱们人为规定,next[0]=-1,next[1]=0
(2)来到i位置,0–i-1的next信息已经具备,咱只需要根据前面的状况推导i位置的next信息即可
(3)咱只需要确认y=next[i-1]位置的字符,与i-1位置的字符c是否相等?【next[i-1]>0的话】
如果相等,那next[i]=next[i-1]+1;
如果不相等,那就继续看y=next[y]位置的字符,与i-1位置的字符c是否相等?
如果next[y]<=0,说明,往前没有字符了,直接令next[i]=0;一个都没有匹配的
(4)i++,直到i越界
一定要把上面next的含义搞清楚,逻辑整明白了,咱们才能手撕代码,否则这个代码你都不知道在干啥
//KMP算法预备知识:字符串match的每一个位置i之前的字符串,前缀与后缀匹配的最大长度是多少?
//next数组啥意思呢?**next[i]代表:match串的i位置前面,前缀与后缀相匹配的最大长度。**
//【1】即0--i-1范围上前后缀匹配的最大长度。
//【2】next[i]还代表,**0--i-1范围上第一个与i位置不匹配的位置。**
public static int[] writeNextInfo(char[] m){
int M = m.length;
int[] next = new int[M];
//(1)咱们人为规定,**next[0]=-1,next[1]=0**
next[0] = -1;//next[1]=0默认
int i = 2;//2开始
int y = next[i - 1];//它是0--i-2已经匹配的长度,也是咱们直接对标跟i-1没有匹配上的那个位置
while (i < M){
//(2)来到i位置,0--i-1的next信息已经具备,咱只需要根据前面的状况推导i位置的next信息即可
//(3)咱只需要确认y=next[i-1]位置的字符,与i-1位置的字符c是否相等?【next[i-1]>0的话】
if (m[y] == m[i - 1]) {
//**如果相等**,那next[i]=next[i-1]+1;
next[i++] = y + 1;//它是0--i-2已经匹配的长度+1
y++;//这个别忘了--这样的话,咱们可以接着往下比i+1位置
}
//**如果不相等**,那就继续看y=next[y]位置的字符,与i-1位置的字符c是否相等?
else if (next[y] > 0) y = next[y];//往前整
//如果next[y]<=0,说明,往前没有字符了,直接令next[i]=0;一个都没有匹配的
else next[i++] = 0;//到头了都没有匹配上。
//(4)i++,直到i越界
}
return next;
}
这代码很简洁,关键在逻辑,需要你整清楚,捋清了,就好理解代码,也好写代码,第一次见不会没关系,多看看我的举例。
慢慢你就能理解了。【就连我学了多次了,都容易写错,这种题目,超级难,笔试不会出现,面试可能会,你准备了,可能面试就拿下了,哪怕说思想也行。】
KMP算法,舍弃思想,前缀对比过的那串就不要重复对比了
我们来讲KMP算法怎么加速寻找匹配字符串的
根据上面所述,咱们准备好m字符串的next数组信息(next[i]是i之前前后缀匹配的最大长度,next[i]也是0–i-1上第一个不匹配的位置)
咱们先说这个KMP算法的对比流程:
(0)对match串构建next信息
(1)咱们拿m的y=0位置开始与s的x=i位置一个一个往后面匹配
(2)如果发现x字符与y字符相等!则x++,y++;回到(2)继续比对
(3)如果发现x字符与y字符不相等!那直接宣告s中i–j-1这一段,谁开头都没法匹配处m串来!【舍弃i–j-1这一段开头的情况,不要对比了(图中粉色那段)】,下一次直接默认s从j开头从新匹配
(4)同时,还不是让y直接退回到m串的0位置从头匹配,而是让y=next[y](暂时叫它y2),直接判断x字符是否等于y2字符
若是相等,去(2)继续对比
若是不相等,去(3)继续对比
(5)如果不等的情况下发现next[y]=-1的话,说明m从y=0位置也跟x字符不相等,x++即可;去s的后面继续对比;
(6)如果x越界,或者y越界,发现y=M,则说明整个m串都匹配好了,返回结果x-y。因为x-m的长度M就是s中首个匹配上的位置i。
咱们再来解释,为啥KMP这么做就能加速呢?
(0)对match串构建next信息
(1)咱们拿m的y=0位置开始与s的x=i位置一个一个往后面匹配
(2)如果发现x字符与y字符相等!则x++,y++;回到(2)继续比对
解释:这没啥问题吧,你拿到字符串,自然从俩串的开头位置匹配,实际上上面的x也是从s的0位置开始对比的
(3)如果发现x字符与y字符不相等!那直接宣告s中i–j-1这一段,谁开头都没法匹配处m串来!【舍弃i–j-1这一段开头的情况,不要对比了(图中粉色那段)】,下一次直接默认s从j开头从新匹配
解释: 为啥呢?跟下面(4)的解释一块看!
(4)同时,还不是让y直接退回到m串的0位置从头匹配,而是让y=next[y](暂时叫它y2),直接判断x字符是否等于y2字符
若是相等,去(2)继续对比
若是不相等,去(3)继续对比
解释:
原因是next数组的含义:next[y]是y之前的前缀后缀匹配的最大长度,next[y]也是0–y-1上第一个不匹配的位置
图中y前面的L2那段后缀=L3那段前缀,范围是0–y2-1,自然前缀的长度=后缀的长度就是y2,也就是next[y],这个y2也是m的0–y-1上第一个与y字符不匹配的位置。
既然y不等于x,我们就宣告i开头的字符串是无法匹配出m来的。
在暴力解中,x就需要直接回退到i+1处,y需要回退到m的0位置,又重头对比……这真的有必要吗?
有了next数组,咱能就不需要回退那么多了!!!
有了next数组,咱能就不需要回退那么多了!!!
有了next数组,咱能就不需要回退那么多了!!!
其实,i–j-1那段谁开头都没法匹配出m串来,因为L2=L1=L4=L3,现在x不等于y字符,i+1之后谁开头,都起码要搞出L4字符串出来,与L3相对应,那自然是只有L1能作为下一个能匹配的L4了【你仔细品一下,是不是】【你看下图红色那段L4,未来要与现在黑色L1重合,这样才是未来能匹配m的真实状况】,因此,下一次最差情况也是从j为开头的字符串,才有课鞥与m匹配,这样也才能保证现在的j–x-1(L1)作为最新的能匹配的L4段,去对应m串中的L3段。
所以默认下次开头为j的话,就 轻松舍弃暴力解中i–j-1这段的重复对比。
既然下一次能匹配的L4是现在的L1,L1=L3,那么y也不需要从m的0重头对比,所以y只需要去next[y]位置(y2那)继续对比s中的x字符【黑色x】,看看j开头的字符串后续能否与m匹配上?
于是,这样的话轻松舍弃暴力解中0–y2-1这一段重复的对比
这(3)(4)这段解释是就是KMP算法的核心关键之处!明白了这个舍弃思想,咱就能搞懂这个加速的本质了。
当然但凡遇到舍弃的思想,都是非常非常困难的,但是要捞清楚才行。
(5)如果不等的情况下发现next[y]=-1的话,说明m从y=0位置也跟x字符不相等,x++即可;去s的后面继续对比;
解释:next[y]=-1,说明现在y在m的0位置,,x和y都不相等,等价于第一个y就没法和x对比,x必须++。这本就是常规操作。
——这儿我都想了很久才想通,但是就是最开始第一个x和y对不上,只能动x啊……没啥问题
(6)如果x越界,或者y越界,发现y=M,则说明整个m串都匹配好了,返回结果x-y。因为x-m的长度M就是s中首个匹配上的位置i。
xy一个越界,没字符了,如果y已经对比完了呢?就说明没问题,否则还没匹配完,返回-1。
因为i–x=m的0–M-1,所以呢x-M就是开头匹配上m的开头位置i。返回去就行。
咱们举例说明KMP算法对比半天失败的例子
s=a a b a a c t a a b a a f…
m=a a b a a c t a a b a a k
i = 0 1 2 3 4 5 6 7 8 9 0 1 2
首先对m建立next数组信息:
s =a a b a a c t a a b a a f…
m=a a b a a c t a a b a a k
i = 0 1 2 3 4 5 6 7 8 9 0 1 2
(1)x=0,y=0开始对比,一直增加xy到x=12,y=12,发现x不等于y
(2)看y=next[y]=next[12]=5,咱们直接对比m的y=5位置与s的x字符相等吗?【舍弃0–4重复对比,因为0–4等于x-5–x-1这段了】看下图绿色的y,x与y并不相等
(3)看y=next[y]=next[5]=2,咱们直接对比m的y=2位置与s的x字符相等吗?【舍弃0–1重复对比,因为0–1等于x-2–x-1这段了】看下图粉色的y,x与y并不相等
(4)看y=next[y]=next[2]=1,咱们直接对比m的y=1位置与s的x字符相等吗?【舍弃0–0重复对比,因为0–0等于x-1–x-1这段了】看下图橘色的y,x与y并不相等
(5)看y=next[y]=next[1]=0,y回到m原点,咱们直接对比m的y=0位置与s的x字符相等吗?
仍然不相等,y已然在原点了,next[0]=-1,所以动x吧,x++滑动继续匹配新的可能性……
如果此时x越界,显然y!=M,说明s中就没有可能
咱们举例说明KMP算法对比半天成功的例子
s =a a b a a c t a a b a a f a a b a a c t a a b a a k
m=a a b a a c t a a b a a k
i = 0 1 2 3 4 5 6 7 8 9 0 1 2
比上面失败的例子呢,就s后面多了m呗,从13位置开始算就能匹配m出来
所以上面接着(5)对比
(6)x++,y++,不断,最后y越界,自然y=M,发现就OK了,哈哈哈哈!!!!!!
手撕KMP算法的代码
其实关键还是要回顾match串的next信息数组的建立代码:
//KMP算法预备知识:字符串match的每一个位置i之前的字符串,前缀与后缀匹配的最大长度是多少?
//next数组啥意思呢?**next[i]代表:match串的i位置前面,前缀与后缀相匹配的最大长度。**
//【1】即0--i-1范围上前后缀匹配的最大长度。
//【2】next[i]还代表,**0--i-1范围上第一个与i位置不匹配的位置。**
public static int[] writeNextInfo(char[] m){
int M = m.length;
int[] next = new int[M];
//(1)咱们人为规定,**next[0]=-1,next[1]=0**
next[0] = -1;//next[1]=0默认
int i = 2;//2开始
int y = next[i - 1];//它是0--i-2已经匹配的长度,也是咱们直接对标跟i-1没有匹配上的那个位置
while (i < M){
//(2)来到i位置,0--i-1的next信息已经具备,咱只需要根据前面的状况推导i位置的next信息即可
//(3)咱只需要确认y=next[i-1]位置的字符,与i-1位置的字符c是否相等?【next[i-1]>0的话】
if (m[y] == m[i - 1]) {
//**如果相等**,那next[i]=next[i-1]+1;
next[i++] = y + 1;//它是0--i-2已经匹配的长度+1
y++;//这个别忘了--这样的话,咱们可以接着往下比i+1位置
}
//**如果不相等**,那就继续看y=next[y]位置的字符,与i-1位置的字符c是否相等?
else if (next[y] > 0) y = next[y];//往前整
//如果next[y]<=0,说明,往前没有字符了,直接令next[i]=0;一个都没有匹配的
else next[i++] = 0;//到头了都没有匹配上。
//(4)i++,直到i越界
}
return next;
}
然后根据KMP的法流程,很简单手撕出来了
咱们先说这个KMP算法的对比流程:
(0)对match串构建next信息
(1)咱们拿m的y=0位置开始与s的x=i位置一个一个往后面匹配
(2)如果发现x字符与y字符相等!则x++,y++;回到(2)继续比对
(3)如果发现x字符与y字符不相等!那直接宣告s中i–j-1这一段,谁开头都没法匹配处m串来!【舍弃i–j-1这一段开头的情况,不要对比了(图中粉色那段)】,下一次直接默认s从j开头从新匹配
(4)同时,还不是让y直接退回到m串的0位置从头匹配,而是让y=next[y](暂时叫它y2),直接判断x字符是否等于y2字符
若是相等,去(2)继续对比
若是不相等,去(3)继续对比
(5)如果不等的情况下发现next[y]=-1的话,说明m从y=0位置也跟x字符不相等,x++即可;去s的后面继续对比;
(6)如果x越界,或者y越界,发现y=M,则说明整个m串都匹配好了,返回结果x-y。因为x-m的长度M就是s中首个匹配上的位置i。
//KMP算法的对比流程:
public static int KMPSearchMatchInStr(String s, String match){
if (s == null || s.equals("") || s.length() < match.length()) return -1;
int N = s.length();
int M = match.length();
char[] str = s.toCharArray();
char[] m = match.toCharArray();
//对match串构建next信息
int[] next = writeNextInfo(m);
//(1)咱们拿m的y=0位置开始与s的x=i位置一个一个往后面匹配
int x = 0;
int y = 0;
while (x < N && y < M){
//(2)如果发现**x字符与y字符相等**!则x++,y++;回到(2)继续比对
if (str[x] == m[y]){
x++;
y++;//往后滑动即可
}
//(3)如果发现**x字符与y字符不相等**!那直接宣告s中i--j-1这一段,谁开头都没法匹配处m来!
// 【省掉i--j-1这一段开头的情况,不要对比了】,下一次直接默认s从j开头从新匹配
//(4)同时,还不是让y直接退回到m的0位置从头匹配,而是让**y=next[y]**(暂时叫它y2),
// 直接判断x字符是否等于y2字符
//若是相等,去(2)继续对比
//若是不相等,去(3)继续对比
//(5)如果不等的情况下发现next[y]=-1的话,说明m从y=0位置也跟x字符不相等,x++即可;去s的后面继续对比
else if (y == 0) x++;
else y = next[y];//如果y>0的话,继续回去对比
}
//(6)如果x越界,或者y越界,发现y=M,则说明整个m都匹配好了,返回结果x-y。
// 因为x-m的长度M就是s中首个匹配上的位置i。
return y == M ? x - y : -1;
}
其实KMP算法的代码是非常非常简单的,关键就是搞定match的next数组信息,才能轻松搞定KMP算法。
KMP对比过程中,x总是在++的进程中,哪怕y往回退也退得少,整体x走一遍o(n)就能匹配完,故算法复杂度整体o(n),比暴力解好多了吧。
然后就可以测试了,
public static void test(){
String s = "aabaactaabaafaabaaaabaakx";
String match = "aabaaaabaak";
char[] m = match.toCharArray();
int[] next = writeNextInfo(m);
for(Integer i : next) System.out.print(i +" ");
int ans = KMPSearchMatchInStr(s, match);
System.out.println();
System.out.println(ans);
}
public static void main(String[] args) {
test();
}
结果
next和咱们匹配到的位置
-1 0 1 0 2 2 2 2 3 4 5
13
总结
提示:重要经验:
1)KMP算法的关键是match串的预设数组信息next数组,next数组啥意思呢?next[i]代表:match串的i位置前面,前缀与后缀相匹配的最大长度。next[i]还代表0–i-1范围上第一个与i位置不匹配的位置。
2)然后KMP算法的加速在于舍弃前缀匹配过那段重复对比,是复杂度加速到了o(n)
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。