题目:28. 实现 strStr()
在一个串中查找是否出现过另一个串,这是KMP的看家本领
问题描述
实现 strStr() 函数。给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1:
输入: haystack = “hello”, needle = “ll”
输出: 2
示例 2:
输入: haystack = “aaaaa”, needle = “bba”
输出: -1
说明:当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。
解法1:直接顺序遍历整个字符串,用subString(startIndex,endIndex)找,找到第一个匹配的就返回
这样回答,面试中一定会问你,还有没有更好的解法
class Solution {
public int strStr(String haystack, String needle) {
for (int start =0; start< haystack.length() - needle.length()+1;start++){
if(haystack.substring(start,start+needle.length()).equals(needle))
return start; // 这里的start就是数组下标,直接返回就好了
}
return -1;
}
}
一般来说,字符串的题目是会用toCharArray()转换为字符数组的,但是这里没必要了
时间复杂度:O((N - L)L)O((N−L)L),其中 N 为 haystack 字符串的长度,L 为 needle 字符串的长度。内循环中比较字符串的复杂度为 L,总共需要比较 (N - L) 次。
空间复杂度:O(1)O(1)。
解法2:双指针法(两个字符串都用下标遍历,从刚才的subString equals比较整个字符串,变为比较各个字符,有不匹配的就回溯,降低时间复杂度)-线性复杂度
上一个方法的缺陷是会将 haystack 所有长度为 L 的子串都与 needle 字符串比较,实际上是不需要这么做的。
首先,只有子串的第一个字符跟 needle 字符串第一个字符相同的时候才需要比较。
其次,可以一个字符一个字符比较,一旦不匹配了就立刻终止。
如下图所示,比较到最后一位时发现不匹配,这时候开始回溯。需要注意的是,pn 指针是移动到 pn = pn - curr_len + 1 的位置,而 不是 pn = pn - curr_len 的位置。
这时候再比较一次,就找到了完整匹配的子串,直接返回子串的开始位置 pn - L 。
class Solution {
public int strStr(String haystack, String needle) {
if(needle==null || needle.length()==0) return 0; // 特殊情况优先处理
int n = haystack.length(); int L = needle.length();
int pn=0;
while(pn < n - L +1){
while (pn < n - L +1 && haystack.charAt(pn) != needle.charAt(0) ) pn++; // 首字符不同,不断往后走
int targetLength=0; int pL=0;
while(pn < n && pL<L && haystack.charAt(pn) == needle.charAt(pL) ){
pn++;
pL++;
targetLength++;
}
if(targetLength == L) return pn-targetLength; // 返回起始位置 targetLength和L相同,这里写pn-targetLength方便理解
pn = pn - targetLength +1; // 向后移动一位
}
return -1; // 在循环中没有结束,就是没有找到,循环体外面返回-1
}
}
while(pn < n && pL<L && haystack.charAt(pn) == needle.charAt(pL) ){
这一句,不能写成 while(pn < n -L +1 && pL<L && haystack.charAt(pn) == needle.charAt(pL) ){
这样写到末尾就不让累加了,这个错误的
时间复杂度:最坏时间复杂度为 O((N - L)L)(每次都要最后一个字符不同才知道不匹配,和subString equals 比较一样),最优时间复杂度为 O(N) (每次都是第一个字符不同就知道不匹配)。
空间复杂度:O(1)。
解法三:KMP算法 next数组用于记录已经匹配的文本内容
KMP的经典思想就是:「当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。
首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!
「之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。」
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
接下来就要说一说怎么计算前缀表。
其他(了解即可):什么是Rabin-Karp算法,它是一个常见的字符串匹配算法,通常学过数据结构的人知道,常见的字符串匹配算法有BF,KMP算法,其中KMP算法性能是比较优良的,在这里讲的是Rabin-Karp(简称RK)算法,性能也是很好的,利用了hashcode的思想,将比较的字符串进行求hashcode的值,我们知道,若两个字符不同,他们的hashcode值可能相同(几率很小),若两者hashcode值不同,必推出两个字符串不同,所以利用这个思想就设计了RK算法。
KMP算法:next数组
RK算法:哈希