引言
题目: 28. 实现 strStr()
实现 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:双指针暴力(BF(Brute Force))
class Solution {
public int strStr(String haystack, String needle) {
if(haystack.length()<needle.length()) return -1;
int l=0;
while(l<haystack.length()-needle.length()+1){
if(needle.equals(haystack.substring(l,l+needle.length()))) return l;
l++;
}
return -1;
}
}
解法2:哈希码(RK(Rabin-Karp))
这份代码能基本体现思想以及解决大部分用例,但是对于一些长度超级长的字符串,会出现溢出导致结果不对,我一直没处理好,先这样吧。
class Solution {
public int strStr(String haystack, String needle) {
if(haystack.length()<needle.length()) return -1;
long hashcode_needle=0,hash_temp=0;
for(int i=0;i<needle.length();i++){
hashcode_needle+=((needle.charAt(i)-'a')*Math.pow(26,i));
hash_temp+=((haystack.charAt(i)-'a')*Math.pow(26,i));
}
int l=0;
if(hash_temp==hashcode_needle) return l;
while(l<haystack.length()-needle.length()){
hash_temp-=(haystack.charAt(l)-'a');
hash_temp/=26;
hash_temp+=((haystack.charAt(l+needle.length())-'a')*Math.pow(26,needle.length()-1));
l++;
if(hash_temp==hashcode_needle) return l;
}
return -1;
}
}
KMP算法
引入
为了引入KMP算法以及为了展示KMP算法的优越性,我先举个字符串匹配的例子,例如还是一开始的那一题
好,如果字符串是这个样子,我们最暴力的解法是什么,就是逐个去比嘛,先把needle放在最左边,然后一个个去比,当走到下面这一步发现不匹配了
这个时候needle就往右移动一格重新来比嘛
以此类推,这个随便一看就知道费时费力,肯定不好用,这个时候KMP就出来了。
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 暴力算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
正题:KMP
上面说道 KMP 算法主要是通过消除主串指针的回溯来提高匹配的效率的,那么,它是怎么样来消除回溯的呢?就是因为它提取并运用了加速匹配的信息!
举个简单的例子,比如刚刚那个
当逐个匹配到这一步的时候,暴力解法是右移一格重新开始比。KMP就觉得重新开始比好麻烦,它先观察了一下needle串的特征,发现这个时候needle串可以直接跳到下面这一步来比
这可能在你看来只是KMP小小的一步,但是实际上对整个算法流程改进了不止一点点。首先
- 当发现不匹配时,needle串没有按照暴力法那样右移一格重新开始比,而是直接找到一个前缀已经匹配正确的位置和当前位置再去比
- 当needle串这样走的时候,我们就会惊奇的发现haystack串根本不需要一点回溯,直接一次遍历完就好了
为了更好的理解这个过程,我们把上面的题目一步步的执行一遍。
- 两个串从第一个位置开始比,一直右移到第五个位置发现不匹配
- 这个时候needle串跳到下一个位置,继续和haystack下标为4的那个B继续比较
- 发现仍然不匹配,继续跳到下一个位置
- 发现needle的第一个字符也不匹配了,这个时候needle右移一格,重新开始比,也就是只要needle的第一个字符都不匹配就右移一格
- 发现已经匹配了,结束。
整个过程可以发现,haystck是完全没有回溯的,needle也通过跳转那一步减少了大量的回溯过程。那么问题来了,跳转那一步到底是什么原理呢,这个解释起来比较复杂,比较正规的解释就是前缀和后缀相同,比如ABCAB的相同前后缀就是AB,这个时候如果needle是ABCABD,当比较到D的时候不匹配这个时候怎么利用这个公共前后缀来跳转呢。首先我们要搞清楚当执行到这一步的时候,有哪些信息是已知的了:
首先,集然已经开始匹配needle的D了,说明前面ABCAB肯定已经是匹配的,如下图:
这个时候就可以用到前后缀了,既然ABCAB已经重合了,那么我们就可以直接把前缀移动到后缀的位置上就好了
跳转的原理就是这么简单,紧接着我们就会发现,跳转的实现似乎只提取了needle串的特征,当某个位置不匹配时,needle串跳转到下一个位置继续比,如果跳到开头还不匹配就用needle的开头和haystack的下一个位置比。这也正是KMP算法厉害的另外一个点,只要提取needle串的特征,就可以和任意一个haystack串去匹配。
next数组
上面说了那么多的跳转,其实官方一点就是定义一个next数组,比如上面的图,D的下标是5,D不匹配时跳转过来比较的是C,下标为2,所以next[5]=2。而第一个元素的next也就是next[0]设为-1,这样如果跳转的下标是-1就在haystack上右移一个。
next数组代码如下
public int[] Getnext(String t,int[] next)
{
int j=0,k=-1;
next[0]=-1;
while(j<t.length()-1)
{
if(k == -1 || t.charAt(j) == t.charAt(k))
{
j++;k++;
next[j] = k;
}
else k = next[k];
}
return next;
}
当 j 的值为 0 或 1 的时候,它们的 k 值都为 0,即 next[0] = 0、next[1] =0。但是为了后面 k 值计算的方便,我们将 next[0] 的值设置成 -1。代码里面k的值可以理解为公共前后缀的长度。
needle串简化定义为字符串t
- 当t[k]==t[j],那肯定next[j+1]=k+1,因为前k个都是重合的。
- 当t[k]!=t[j]时,k=next[k],这一步我看懂了但是我讲不清楚,正应了那句话只可意会不可言传。
next数组的一点改进
我们看这个例子
如果按照上面的代码走的话,next[5]=2,但是我们发现其实t[5]和t[2]是一样的,即使5先跳到了2,它下一步还是要跳到next[2],所以这里可以改进一下。
public int[] Getnext(String t,int[] next)
{
int j=0,k=-1;
next[0]=-1;
while(j<t.length()-1)
{
if(k == -1 || t.charAt(j) == t.charAt(k))
{
j++;k++;
if(t.charAt(j) == t.charAt(k))//当两个字符相同时,就跳过
next[j] = next[k];
else
next[j] = k;
}
else k = next[k];
}
return next;
}
KMP代码
```java
int KMP(String s,String t)
{
int[] next=new int[t.length()];
int i=0,j=0;
next=Getnext(t,next);
while(i<s.length() && j<t.length())
{
if(j==-1 || s.charAt(i)==t.charAt(j))
{
i++;
j++;
}
else j=next[j]; //j回退。。。
}
if(j>=t.length())
return i-t.length(); //匹配成功,返回子串的位置
else
return -1; //没找到
}