28. 找出字符串中第一个匹配项的下标
本题是KMP算法的一个最简单的应用。KMP算法主要的用处即为找到匹配字符串的下标。
我们用题目中的haystack和needle字符串来讲解KMP算法的原理。给needle字符串建立一个前缀表,存储在每个字符之前(包括该字符)最长的相同前后缀的长度。之后再用这个前缀表来匹配needle字符串,当已经匹配了一段相同的字符串,突然匹配到一个不相同的字符时,可以倒回到不相同的字符的前一个字符的最长相同前缀之后的一个位置。
为什么是这个位置呢?因为对于这个不相同的字符的前一个字符和它之前的这段的字符,它的某长度的后缀与needle某长度的前缀是相同的字符(这个长度存在前缀表中),因此在匹配needle时就可以从这个长度的前缀之后的第一个字符开始判断。
本题调用一个方法来求前缀表(即next数组),之后用i指针遍历haystack字符串,j指针指向needle字符串,匹配时j指针后移,不匹配时j指针返回上述位置进行比较,j指到next数组的最后一个元素时匹配完成,返回下标。如果在循环结束还没有返回下标则,说明匹配失败,返回-1。
在next数组中,next[i]即为下标为i之前最长的相同前后缀的长度。i用来遍历,同时也是指向后缀末尾的指针;指针j用来指向前缀末尾。对于第一个字符,没有前后缀,next[0] = 0。指针j在一开始也指向0。在循环中,当字符匹配时,j递增;不匹配时,与haystack和needle的匹配原理相同,j退回到next[j - 1],因为i-1及之前的字符的next[j - 1]长度后缀与最前面的next[j - 1]长度的前缀仍然是相同的,而next[j - 1]刚好是这个前缀之后的元素,当前遍历的i是这个后缀之后的元素,所以如果两个元素相同则可以匹配了,不相同的话就继续停留在这个while循环里,继续重复刚刚的流程。
class Solution {
public int strStr(String haystack, String needle) {
if (needle.length() == 0) return 0;
int[] next = new int[needle.length()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && needle.charAt(j) != haystack.charAt(i)) {
j = next[j - 1];
}
if (needle.charAt(j) == haystack.charAt(i)) {
j++;
}
if (j == needle.length()) {
return i - needle.length() + 1;
}
}
return -1;
}
private void getNext(int[] next, String s) {
int j = 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;
}
}
}
459. 重复的子字符串
这道题也是一道查找子串的题,仍然可以使用KMP算法。本题中,最长相等前后缀不包含的子串就是最小重复子串,原理见代码随想录。因此我们用与上一题中同样的方法获取next数组,如果输入字符串的最长相等前后缀(即为next[length - 1],整个字符串的最长相等前后缀)不为0,且除去这个前后缀的剩余部分能被字符串长度整除,就说明这个字符串是完全由重复的子串组成的。
class Solution {
public boolean repeatedSubstringPattern(String s) {
int length = s.length();
if (length == 0) return false;
int[] next = new int[length];
getNext(next, s);
if (next[length - 1] != 0 && length % (length - (next[length - 1])) == 0) {
return true;
}
return false;
}
private void getNext(int[] next, String s) {
int j = 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;
}
}
}
双指针回顾
双指针法在做数组、字符串、链表、N数之和类的题时都有涉及。
对于数组,在27. 移除元素 中,快慢指针法使得时间复杂度更低。
对于字符串,在344. 反转字符串中利用双指针法交换元素,也是降低了时间复杂度;在151. 反转字符串中的单词中,使用start和end指针进行反转字符串和删除冗余空格的操作。
对于链表,可以通过改变next指针直接反转链表,在链表中求环时也会用到快慢指针法。
对于三数之和,可以在循环中利用left和right指针寻找合适的大小,对于四数之和,先将两个数的和列出来,再用left和right指针找到剩下的两个数,更大的N数之和也都是这种做法,先将前几个数的和列出来,最后用双指针找剩下的两个数。
在上述双指针的应用中,链表的某些题目必须使用双指针才能解,其它的题目都是利用双指针来降低时间复杂度。