一、前言
从九月一开始日刷算法,每日三题稳定收获 LeetCode 21积分,在今天刷到 28. 实现 strStr()时, 最开始使用了暴力破解,双重循环(下面有具体介绍),但是在我看评论区的时候,发现这道题是 KMP的经典题目,但是我连什么是 KMP都不知道,接下来我会为和我一样的算法小白分享一下我理解的 KMP算法和相关题目的讲解
二、LeetCode 28. 实现 strStr()
题目描述
解题思路(暴力破解)
- 根据文中所说,在字符串 haystack中找出 needle,找到了就返回第一个字符串出现位置的下标
- 这个时候,我灵机一动,这不就是两层循环的事情吗,第一层循环遍历 haystack,然后第二次循环遍历 needle,判断字符串是否相等就可以了
- 于是我开始对字符串下手了,先将两个字符串都转为数组
- 第一层 for循环从下标 0开始,终止条件是 haystack的长度
- 第二个 while循环的开始条件是当前外层循环的字符 == needle的第一个字符
- 相等 i++,n++
- 不相等 结束 while循环
- 每次在去判断一下 n是否 == needle的长度
- 相等则表明 needle在字符串 haystack中
假设目标字符串 haystack为 abcabde,needle字符串为 abd
下图为暴力破解的流程图
代码实现
public static int strStr(String haystack, String needle) { // 字符串转为 char数组 char[] chars = haystack.toCharArray(); char[] chars1 = needle.toCharArray(); // 遍历字符串 haystack for (int i = 0; i < chars.length; i++) { // n表示第二层for循环的数组下标, m表示第一层for循环的数组下标 int n = 0; int m = i; // 遍历 needle对应的 char数组,判断 chars[m] == chars1[n] while(m < chars.length && n < chars1.length && chars[m] == chars1[n]){ m++; n++; } // 判断遍历的第二个字符串 needle的数组是否遍历完成 // 只有 needle包含在 haystack中,才能使得 n == chars.length if (n == chars1.length && chars[m - 1] == chars1[n - 1]){ return i; } } return -1; } 复制代码
提交结果
小题总结
我知道自己的方法属于暴力破解,正确方式不应该这样做,所以我看了评论区和题解区发现有以下几种题解方式
- 使用 String.substring(int beginIndex, int endIndex)方法截取字符串的
- 内置函数(方法), 例如: String.indexOf(String str)
- 像我一样两层循环
- 双指针
- KMP
看到 KMP的时候我真的是懵的,因为听都没听说过,在看下面的评论,差点吓退我,但是 内卷人是这么容易被吓退的吗?赶紧学起来
三、KMP算法
由来
外国人: Knuth,Morris和Pratt发明了这个算法,然后取它三个的首字母进行了命名。所以叫做KMP
用处
主要应用在 字符串匹配中
最长公共前后缀
将字符串拆分成以首字符开始的各个子串,然后依次判断,下标 n与下标 length - n - 1的元素是否相等,若有 m个连续相等的字符,则最长公共前缀为 m
例如字符串:AABAAF的公共前后缀长度如下图所示,红色代表不相同,蓝色代表元素相同
知道了公共前缀, 相应的可以利用我们获取到的公共前缀表来找到当字符不匹配的时候指针应该移动的位置,如下图所示:
下图来自于 代码随想录
核心:next数组
next数组就是用来存储公共前缀的个数的, 例如上面的例子,他的next数组结果就应该是
int[] next = {0,1,0,1,2,0} 复制代码
思路分析
- 前置条件, 字符串 s,前缀数组 int[] next
- 设置一个整数 j代表最长公共前后缀的个数
- 首先是初始化,我们的 next数组第一个元素肯定是 0
- 然后去 for循环我们的字符串,这里需要注意的是我们的for循环是从下标 1开始的
- 判断 j必须大于 0不然的话在回溯过程中会发生 越界的情况,还要判断元素是否相等
- 若不相等则 j回溯到上一个 next数组下标
- 这里需要注意的是要用 while循环,因为可能会一直进行回溯操作
- 当 s.charAt(j) == s.charAt(i)时,代表最长前后缀元素个数 +1,所以 j++
- 最后将 j的值赋给数组 next[i]
代码展示
public static int[] getNext(String s,int[] next){ int j = 0; // 初始化下标 0 next[0] = 0; for (int i = 1; i < s.length(); i++) { while(j > 0 && s.charAt(j) != s.charAt(i)){ j = next[j - 1]; } if (s.charAt(j) == s.charAt(i)){ j++; } next[i] = j; } return next; } 复制代码
四、LeetCode 28. 实现 strStr()
现在我们重新根据 KMP算法来做一遍 LeetCode 28题
思路分析
- 判断 needle长度是否等于 0,如果等于 0则返回 0
- 判断 needle长度是否大于 haystack,如果大于则返回 -1
- 创建 next数组
- 执行我们的 getNext方法
- 设置 next数组的指针 j = 0
- for循环遍历 haystack
- next指针大于 0时判断字符串 haystack下标 i位置的元素是否等于字符串 needle下标 j的位置
- 不相等则 j进行回溯操作
- 一样的, 回溯可能多次,所以使用 while循环
- 如果相等
- j++
- 如果 j == needle的长度,则代表 haystack包含字符串 needle
- 返回 i - j + 1;
代码展示
public static int strStr2(String haystack, String needle) { // 如果 needle为 “” 则返回0 if (needle.length() == 0){ return 0; } // 如果 needle的长度大于 haystack则返回 -1 if (needle.length() > haystack.length()){ return -1; } // 生成 next数组 int[] next = new int[needle.length()]; getNext(needle,next); // 标记指针在 needle回退后的位置 int j = 0; for (int i = 0; i < haystack.length(); i++) { // 判断当前位置 while(j > 0 && haystack.charAt(i) != needle.charAt(j)){ j = next[j - 1]; } if (haystack.charAt(i) == needle.charAt(j)){ j++; } if (j == needle.length()){ return i - j + 1; } } return -1; } public static int[] getNext(String s,int[] next){ int j = 0; // 初始化下标 0 next[0] = 0; for (int i = 1; i < s.length(); i++) { while(j > 0 && s.charAt(j) != s.charAt(i)){ j = next[j - 1]; } if (s.charAt(j) == s.charAt(i)){ j++; } next[i] = j; } return next; } 复制代码
提交结果
虽然但是,没有暴力破解的速度快,但是确实也学到了一种算法
五、String.index(String str)
在 LeetCode评论区有人直接使用了编程语言封装好的方法或者函数,我以前也只是使用过,接下来我们一起看一下源代码
我使用的测试代码如下所示
String haystack = "mississippi", needle = "issip"; int i1 = haystack.indexOf(needle); 复制代码
对应的源码方法重载跳转如下所示:
- 先进入 indexOf(String str)方法
- 跳转进入 indexOf(String str, int fromIndex)
- 这个时候 fromIndex == 0
- 跳转进入 inexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)
- value表示 haystack元素的 char数组
- value.length表示 haystack的长度
- str.value表示 needle元素的 char数组
- str.value.length表示 needle的长度
- 接下来我们看 indexOf方法的具体实现
String.indexOf()方法详解
因为我没有下载 java8源码,所以直接在这里进行注释标注了
// source: 当前字符串元素 // sourceOffset: 偏移量 // sourceCount: 当前字符串长度 // target: 目标字符串 // targetOffset:目标字符串偏移量 // targetCount: 目标字符串长度 // fromIndex: 索引位置 static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) { if (fromIndex >= sourceCount) { return (targetCount == 0 ? sourceCount : -1); } if (fromIndex < 0) { fromIndex = 0; } // 如果目标字符串的长度为 0则直接返回 索引位置 (我们这里因为之前方法重载 fromIndex为 0 所以返回 0) if (targetCount == 0) { return fromIndex; } // 获取目标 首字符 char first = target[targetOffset]; // 我们这里就是 源字符串长度 - 目标字符串长度, sourceOffset == 0 int max = sourceOffset + (sourceCount - targetCount); // 循环,i == 0 // 加入循环到了 max的索引位置还没有找到和 目标字符串首字符相等的元素,则源字符串中肯定不包含目标字符串 for (int i = sourceOffset + fromIndex; i <= max; i++) { // 搜索首字符所在位置 if (source[i] != first) { while (++i <= max && source[i] != first); } // 找到了首字符,判断其余字符位置是否相等 if (i <= max) { int j = i + 1; // 根据目标字符串的长度截取源字符串从下标 i开始相同长度的字符串 int end = j + targetCount - 1; // 遍历判断 for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++); // 如果到最后一个元素都相同,则代表源字符串内包含目标字符串 if (j == end) { // 这里就直接返回了 i 因为源字符串偏移量为 0 return i - sourceOffset; } } } return -1; } 复制代码
使用该源码方式做 LeetCode 28题
public static int strStr(String haystack, String needle) { // 如果 needle为 “” 则返回0 if (needle.length() == 0){ return 0; } // 如果 needle的长度大于 haystack则返回 -1 if (needle.length() > haystack.length()){ return -1; } int max = haystack.length() - needle.length(); char first = needle.charAt(0); for (int i = 0; i <= max; i++) { if (haystack.charAt(i) != first){ while(++i <= max && haystack.charAt(i) != first); } if (i <= max) { int j = i + 1; int end = j + needle.length() - 1; for (int k = 1; j < end && haystack.charAt(j) == needle.charAt(k); j++, k++); if (j == end) { return i ; } } } return -1; } 复制代码
提交结果
六、后续
KMP真的很难理解,建议多看几遍 B站代码随想录,文章也的再好,我感觉也没有视频来的直观,更何况我还是一个初级作者(脸上贴金ing)
接下来,还有一道题 LeetCode 459. 重复的子字符串也可以使用 KMP算法进行解答,感兴趣的可以关注一下我的专栏每日算法
由于算法方面特别影响推荐,我还准备去记录一下自己的刷题,所以每周更新一次算法刷题进度和思路分析,每天三道没有做过的题目,欢迎大家关注