java 收敛算法,KMP算法-劝退刷题的两大门神之一,总算搞懂了

KMP算法

假设字符串str长度为N,字符串match长度为M,M <= N,想确定str中是否有某个子串是等于match的。

时间复杂度O(N)

暴力解

直接遍历,以i(0~N-1)位置开始尝试趟一遍match看看是否能够匹配,如果不匹配就i++继续尝试,如果有一个i是能够匹配的就返回这个i,否则返回-1。最坏情况下,每次都趟了M-1个字符,在最后一个才发现不匹配,然后直到i==N-M的时候才匹配成功。这样时间复杂度就是O(N*M),如果M非常接近N,那么时间复杂度接近O(N^2)。

KMP算法就是将这个匹配过程给优化到O(N),着眼点就是暴力匹配中存在大量的重复比较。

必要铺垫

在介绍KMP算法前先介绍一个概念

字符串i位置字符前的字符串中,前缀字符串和后缀字符串相等的最大长度(

f7930afb2093

image-20210312231809950.png

这个某个位置前的字符串最长相同前后缀字符串长度(这玩意我也不知道学名是啥)就是KMP算法的精髓,0位置的这个值人为规定为-1,因为前面没有字符,1位置这个值人为规定为0,因为前面就一个字符,要求长度要小于前面的字符串长度,所以小于1,设置为0无可厚非。

假设我们已经获得了每个位置的这个长度的值,那么怎么能够优化比较的过程呢?

这就是KMP算法真正优化的地方了,暴力匹配的问题就是i位置开始如果匹配不成功下一次就从i+1位置开始进行重新匹配,从而浪费了之前比较失败的一些字符串特征。

KMP算法流程

之后为了简单起见那个最长...长度我就直接叫maxPrePosEqlLen了

f7930afb2093

image-20210313091708975.png

理解流程的两个关键点

1.为什么str指针不用动,留在x,而match指针要回退到j位置?

f7930afb2093

image-20210313092742955.png

由于str是从i位置开始和match进行匹配,直到x,y位置才匹配不上的,所以str中[i...x-1]和match中[0...Y-1]部分是完全相同的,而match中[0...j-1]部分正好是Y位置的最长前缀字符串,str中[x-j...x-1]部分正好等于Y位置的最长后缀字符串,所以它们肯定相等就不用比对了,让str直接从x开始,match直接从j开始继续比对。

2.像是暴力解很好理解,i位置比对不上,那我从i+1位置继续尝试比对就好了,那肯定是正确的。为什么我现在不从i+1位置开始比对,而是从x-j位置比对呢?这样能保证正确性吗?

证明:为什么从i+1到x-j-1位置开始没有一个是能够匹配match的?

如果这些位置都不可能,那么肯定可以直接淘汰,我从x-j位置开始尝试匹配就合理了,而且大大加速了匹配的速度。

f7930afb2093

image-20210313094058783.png

反证法

假设从i+1到x-j-1中间的某个k位置出发能够匹配match,那么就会有str中k...x-1这一段匹配上match中0...x-k-1这段,由于str中i...x-1和match中0...Y-1这一段完全相等,而k...x-1这一段距离x-k是肯定比j要长的,所以说Y位置就会有一个比j还要长的maxPrePosEqlLen。这是不可能的,因为我们一开始求的maxPrePosEqlLen就是最长的,就是建立在求的maxPrePosEqlLen正确的情况下进行算法的。所以假设不成立,i+1到x-j-1中间任意某个k位置出发必不可能匹配match。所以可以从x-j重新出发。

KMP算法证明成立,可以跳过i+1到x-j-1中间的所有位置,从x-j位置重新出发,这真的起到了加速效果吗,时间复杂度能否收敛到O(N)?

算法实现和分析时间复杂度

算法实现(java)

public static int getIndexOf(String s, String m) {

if (m.length() > s.length()) return -1;

char[] str = s.toCharArray();

char[] match = m.toCharArray();

//maxPrePosEqlLen信息就存在这个数组中i位置的值就对应nexts下标i的值

int[] nexts = getNextArray(match);

int x = 0, y = 0;//x是str的工作指针,y是match的

//为什么循环条件这么写,因为x,y都是从0位置往后匹配的,匹配不上则y回跳

//匹配上则x前走,不管匹配得上还是匹配不上,最终总有至少有一个会到length

while (x < str.length && y < match.length) {

if(str[x]==match[y]){

x++;y++;//匹配上了,都后移,非常合理

}else if(nexts[y]>=0){

//x不动,y移动到最长前缀的后一个位置,假如最长前缀长度为j,那么0~j-1就是最长前缀,所以j就是现在y应该去的地方,这也是为什么maxPrePosEqlLen数组叫做next数组,代表了y下一跳去的地方

y = nexts[y];

}else{

//说明str指针从x-j位置到x位置开始一个都匹配不上,那么x以及x前的位置淘汰,x++,很合理

x++;

}

}

/*

分析:

1.x==str.length&&y==match.length,那么说明x从str.length-match.length位置开始匹配上了match

2.x==str.length&&y!=match.length,那么说明str都走完了,所有位置都淘汰了还是匹配不上match,返回-1

3.x!=str.length&&y==match.length,说明str中间某个串就匹配上了match,这个开始位置是x-match.length

*/

return y==match.length?x-match.length:-1;//结合所有情况,合理

}

时间复杂度

两种求解方式

方式一

f7930afb2093

image-20210313103500286.png

每个分支其实逻辑上都会造成match数组向右移和str对齐,而match最多向右走N-M,所以是O(N)

方式二

f7930afb2093

image-20210313104620442.png

这个分析方式就很骚了,虽然你y是左右横跳,但是x-y的值却是最大为N的,而且处于单调上升的趋势,所以不管你y啥取值,最终也是只会蹦跶N次直到x-y到最大值N就停止。而x最大也是取到N,所以进入循环最多2N次,整体时间复杂度O(N)。

OK,KMP算法整体流程咱们分析过了,只要现在求解next数组的时间复杂度收敛到O(N),那么算法就能够收敛到O(N)了。

求解next数组

next数组也就是maxPrePosEqlLen数组了,因为可以代表y下一跳的位置,所以叫next数组。

分析:match数组长度为M

①如果y==0,那么next[y]==-1,人为规定,没得好说

②如果y==1,那么next[y] ==0,人为规定,没得好说

③y>=2怎么求?

f7930afb2093

image-20210313125540509.png

算法实现和分析时间复杂度

public static int[] getNextArray(char[] match) {

if (match.length == 1) return new int[]{-1};

int[] nexts = new int[match.length];

nexts[0] = -1;

nexts[1] = 0;

int cur = 2;

//这代表的是cur前一个位置的next的值也代表需要和cur比对的match中的下标,理解这个很重要

int preNext = 0;

while (cur < match.length) {

if (match[cur - 1] == match[preNext]) {

//每个cur位置都是要看上一个位置的next的,上个位置设置的时候next = preNext+1,所以就干脆直接修改preNext为preNext+1了

nexts[cur++] = ++preNext;

} else if (preNext>0) {//nexts[preNext]>=0同于preNext>0

//不符合还能往前跳就继续往前跳

preNext = nexts[preNext];

} else {

nexts[cur++] = 0;

}

}

return nexts;

}

时间复杂度

和之前一样用骚操作来推算,preNext虽然反复横跳,但是cur-preNext是要么不变,要么递增的。

f7930afb2093

image-20210313141020005.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是用Java实现KMP算法,用于匹配个数组中第一个相同的值: ```java public class KMP { public static int findFirstMatch(int[] array1, int[] array2) { int[] prefix = computePrefix(array2); int j = 0; for (int i = 0; i < array1.length; i++) { while (j > 0 && array1[i] != array2[j]) { j = prefix[j - 1]; } if (array1[i] == array2[j]) { j++; } if (j == array2.length) { return i - j + 1; } } return -1; } private static int[] computePrefix(int[] array) { int[] prefix = new int[array.length]; int j = 0; for (int i = 1; i < array.length; i++) { while (j > 0 && array[i] != array[j]) { j = prefix[j - 1]; } if (array[i] == array[j]) { j++; } prefix[i] = j; } return prefix; } public static void main(String[] args) { int[] array1 = {1, 2, 3, 4, 5}; int[] array2 = {6, 7, 8, 4, 5}; int index = findFirstMatch(array1, array2); if (index != -1) { System.out.println("The first matching index is: " + index); } else { System.out.println("No matching index is found."); } } } ``` 这里我们定义了一个 `findFirstMatch` 方法,用来查找个数组中第一个相同的值。我们首先计算出 `array2` 的前缀数组 `prefix`,然后使用个指针 `i` 和 `j` 分别指向 `array1` 和 `array2` 的起始位置,如果 `array1[i]` 和 `array2[j]` 相等,则将 `i` 和 `j` 都向后移动一位,如果 `array1[i]` 和 `array2[j]` 不相等,则将 `j` 移动到 `prefix[j-1]` 的位置,然后再继续比较。如果 `j` 移动到了 `array2` 的末尾,则说明找到了第一个匹配的位置,返回该位置即可。如果最终没有找到匹配的位置,则返回 `-1`。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值