2017/11/12更新
优化了代码的写法。新代码附在文章最后。
28. Implement strStr()
28.1 题目描述:
Implement strStr().
Returns the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.
28.2 解题思路:
字符串匹配,一般都是普通解法和KMP算法。KMP算法看起来比较复杂,网上的讲解更是复杂!但其实写起来就一点东西。自己看了好多博客,也没找到特别好的讲解,不过可以参考这篇博客,虽然内容过于累赘,但是讲的很细,理解比较深刻。本文思路二也是参考这篇博客写的,然后根据其解释自己把博客稍微简化了一下,以便加深理解。
2.1 暴力匹配法
暴力匹配,即从haystack开始第一个字符与needle第一个字符匹配,如果匹配,则i++,j++,继续往下走,否则中间一旦发生不匹配,则haystack则从第二个字符开始于needle第一个字符重新开始匹配。重复这个过程,如果中间j走到needle结尾,则匹配成功,如果i走到了haystack结尾处,而j还没有,则匹配失败。
以下所有字符串都以此处为例。
假设s是主串,p是子串,查找p是否在s中,即p是否是s的子串,并返回p在s中的第一个位置。
S=BBCFABCDABFABCDABCDABDE
P=ABCDABD
如果用暴力匹配解决的话,如下流程:
- s[0]!=p[0],没有匹配,外循环执行第2步;
- s[1]!=p[0],没有匹配,执行第3步;
- s[2]!=p[0],没有匹配,执行第4步;
- s[3]!=p[0],没有匹配,执行第5步;
s[4]==p[0],匹配,执行第5.2步,进入内循环;
- s[5]==p[1],匹配,执行第5.3步;
- …
- s[10]!=p[6],没有匹配,此时跳出内循环,进入外循环。
s[5]!=p[0],不匹配,执行第6步;
- …
即重复上述过程。我们可以在暴力匹配的过程发现,当我们发现s[10]!=p[6]不匹配时,又回到s[5]、p[0]重新开始匹配,但s[5]-s[10]之前我们已经检索过了,这样回溯无疑会加大计算量,如果我们人为去匹配,当发现 s[10]!=p[6]不匹配时,我们一般是会从s[8]处重新匹配。而且s[5]、p[0]必然不会匹配,这是因为s[5]==p[1]==B,p[0]==A,p[1]!=p[0],所以s[5]!=p[0]。
那有没有什么方法可以让i不用回溯,只移动j的位置呢,这就是KMP算法的由来。KMP算法通过子串中最长前缀后缀来实现i不回溯。具体见思路二。
2.2 KMP算法
KMP算法。算法一共分为两部分,一部分是求子串的next数组(与最长前缀后缀数组有联系),一部分是根据这个数组来进行主串和子串的匹配。
1、首先先说为什么可以通过子串的最长相同前缀后缀就可以实现i不回溯。
1.1 什么是字符串的最长相同前缀后缀。
比如说,给定字符串str=ABEFCDAB,则其前缀有A,AB,ABE,ABEF,ABEFC,ABEFCD,ABEFCDA,后缀有B,AB,DAB,CDAB,FCDAB,EFCDAB,BEFCDAB,则其最长相同前缀后缀为AB,其长度是2。
1.2 为什么可以使用最长相同前缀后缀。
在思路一中,进行到5.7步时,s[10]与p[6]没有匹配,我们可以发现,子串p在p[6]之前的最长相同前缀后缀是AB,长度为2,这说明,我们不需要将子串p移动一位,将i回溯到5,去进行s[5]与p[0]的比较,只需要保持i不动,将j回溯到2,进行s[10]与p[2]的比较即可(即相当于子串p移动了4位)。这是因为我们已知了p[0]p[1]==p[4]p[5]==AB,而p[4]p[5]==s[8]s[9]==AB,则必然会有p[0]p[1]==s[8]s[9],所以直接比较s[10]与p[2]即可。
所以说,如果我们每次进行匹配时,如若发生了不匹配,我们可以直接查找子串的最长相同前缀后缀数组,不必进行i指针的回溯,直接将j指针回溯到相同前缀的后一位,接着与主串比较即可。这就是为什么要使用最长相同前缀后缀的原因。
1.3 最长相同前缀后缀长度数组与next数组的关系
对于字符串p=ABCDABD来说,其最长相同前缀后缀见下表:
即:
但是我们在KMP算法中,并不是直接用这个数组(当然用也可以)。
这个数组的最长相同前缀后缀长度是以包含当前字符的字符串为基准的,也就是说,当指针j遍历到p[6]时,其最长相同前缀后缀长度是以ABCDABD为基准算法,也就是长度为0。
但在KMP中,当s[10]!=p[6]时,我们当前比较的是字符p[6],所以只要除去p[6]=D后,知道了前面字符串的最长相同前缀后缀长度即可,即ABCDAB为基准,其长度是2。我们称这个长度的数组为next。
所以得到:
可以看到,这两个数组是有联系的,即数组next是赋初值-1后,然后将最长相同前缀后缀长度这个数组整体右移了一位得到的。
1.4 如何求next数组
可以采用递推方式求出。设定字符串为p。
赋初值-1,将其添加到next中,next[0]=-1。
对于j,如果有p[0]p[1]…p[k-1]==p[j-k]p[j-k+1]…p[j-1],则next[j]=k。(这里的next[j]=k,即表示子串p除去p[j]当前位置,在p[0]p[1]…p[j-2]p[j-1]中最长相同前缀后缀长度为k。所以当p[j]与主串s[i]匹配失败时,j直接回溯到next[j],即j=next[j],相当于子串p移动了j-next[j]个距离。如下所示j=6,i=10,匹配失败,j=next[j]=2,相当于p移动了j-next[j]=4个距离,将s[10]与p[2]继续进行匹配。)
对于j+1,则根据next[012…j]求出next[j+1]。
前面我们已有p[0]p[1]…p[k-1]==p[j-k]p[j-k+1]…p[j-1],k=next[j],
①如果p[k]==p[j],则next[j+1]=next[j]+1=k+1;
②如果p[k]!=p[j],判断p[next[k]]与p[j],如果p[next[k]]==p[j],则next[j+1]=next[k]+1;否则继续递归前缀索引。即如果发生p[k]!=p[j],则k=next[k],然后根据p[k]与p[j]是否相等,执行①或②。
③如果k==-1,说明没有相同的前缀后缀,则next[j+1]=0。
这里说明一下为什么步骤②可以用来求next[j+1]。
当p[k]!=p[j]时,说明p[0]p[1]…p[j-1]中没有长度为k+1的相同前缀后缀,则我们试图找一个长度比k+1短的相同前缀后缀。设这个长度为t+1,所以如果t+1这个长度存在,即p[0]p[1]…p[t]==p[j-t]p[j-t]…p[j],则next[j+1]=t+1。
举个例子,P=ABCDABCE,
当j=6,j+1=7时,k=next[j]=2,比较p[k]与p[j](即在字符C之前,其最长前缀后缀长度为2,即p[0]p[1]==p[4]p[5],此时只需比较p[2]与p[6]即可。),发现p[k]==p[j],则next[j+1]=k+1=3。
但是如果p[k]!=p[j]呢?假设字符串p=ABCDABRE,还是j=6时,此时p[k]!=p[j],
C!=R,即ABC!=ABR,所以需要找一个更短的相同前缀后缀,令k=next[k]=next[2]=0,比较p[0]与p[6],发现p[0]!=p[6],继续k=next[k]=next[0]=-1,表示在字符E之前,没有相同的前缀后缀,则next[j+1]=0。
为什么能用k=next[k]寻找长度更短的前缀后缀呢?相当于子串的自我匹配,借用这篇博客的图,
当p[k]!=p[j]时,无法找到长度为k+1的相同前缀后缀,则k=next[k],进行p[next[k]]与p[j]的比较,如果p[next[k]]==p[j],则p[j+1]=next[k]+1=t+1。否则继续k=next[next[k]]。
综上,求next数组的代码如下:
vector<int> findnext(string s)
{
vector<int>next(1,-1);
int k;
for (int i = 1; i < s.length();i++)
{
k = next[i - 1];
while (k>-1)
{
if (s[k] == s[i - 1])
{
next.push_back(k + 1);
break;
}
else
k = next[k];
}
if (k==-1)
next.push_back(0);
}
return next;
}
.
2、然后给出KMP总体算法的流程。
设现在匹配到主串s的i,子串p的j,
如果j==-1,或者s[i]与p[j]匹配,即s[i]==p[j],进行i++,j++,进入下一个字符匹配。
如果j!=-1,并且s[i]与p[j]不匹配,即s[i]!=p[j],则i不变,j=next[j],即将子串p移动j-next[j]个位置。
在1步骤中,如果j==-1,说明s[i]与p[0]匹配失败,需要i++,j++,继续s[i+1]与p[0]的匹配。
综合,匹配代码如下:
int strStr(string haystack, string needle) {
if (needle.length() == 0)
return 0;
if (haystack.length() < needle.length())
return -1;
vector<int>next = findnext(needle);
int i = 0;
int j = 0;
int n = haystack.length();
int m = needle.length();
while (i<n && j<m)
{
if (j == -1 || haystack[i]==needle[j])
{
i++;
j++;
}
else if (j != -1 && haystack[i] != needle[j])
{
j = next[j];
}
}
if (j == needle.length())
return i - j;
else
return -1;
}
给出手动匹配例子。
主串:S=BBCFABCDABFABCDABDDABDE
子串:P=ABCDABD
求子串的next数组过程:
得到next数组如下:
匹配过程:
.
程序运行截图:
2.3 思路三
分为C++和Python
- C++:其实就是对思路一写法上的一个优化,不做赘述。
- Python:利用Python本身的独有的特点,直接进行字符串比较:haystack[i:i+len(needle)]==needle即可。
28.3 C++代码:
1、思路一代码(6ms):
class Solution124 {
public:
int strStr(string haystack, string needle) {
if (needle.length() == 0)
return 0;
if (haystack.length() < needle.length())
return -1;
for (int k = 0; k <= haystack.length() - needle.length();k++)
{
int i = k;
int j = 0;
int flag = 1;
while (j<needle.length())
{
if (haystack[i] != needle[j])
{
flag = 0;
break;
}
i++;
j++;
}
if (flag == 1)
return i - j;
}
return -1;
}
};
2、思路二代码(9ms)
class Solution124_2 {
public:
vector<int> findnext(string s)
{
vector<int>next(1,-1);
if (s.length() == 1)
return next;
next.push_back(0);
int k;
for (int i = 2; i < s.length();i++)
{
k = next[i - 1];
while (k>-1)
{
if (s[k] == s[i - 1])
{
next.push_back(k + 1);
break;
}
else
k = next[k];
}
if (k==-1)
next.push_back(0);
}
return next;
}
int strStr(string haystack, string needle) {
if (needle.length() == 0)
return 0;
if (haystack.length() < needle.length())
return -1;
vector<int>next = findnext(needle);
int i = 0;
int j = 0;
int n = haystack.length();
int m = needle.length();
while (i<n && j<m)
{
if (j == -1 || haystack[i]==needle[j])
{
i++;
j++;
}
else if (j != -1 && haystack[i] != needle[j])
{
j = next[j];
}
}
if (j == needle.length())
return i - j;
else
return -1;
}
};
3、思路三代码(3ms)
class Solution124_1 {
public:
int strStr(string haystack, string needle) {
if (needle.length() == 0)
return 0;
int n = haystack.length();
int m = needle.length();
for (int k = 0; k <= n-m; k++)
{
int j = 0;
while (j < m)
{
if (haystack[k+j] != needle[j])
break;
j++;
}
if (j == m)
return k;
}
return -1;
}
};
28.4 Python代码:
1、思路一代码(45ms)
class Solution(object):
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
if len(needle)==0:
return 0
n=len(haystack)
m=len(needle)
for i in range(0,n-m+1):
j=0
while j<m:
if haystack[i+j]!=needle[j]:
break
j+=1
if j==m:
return i
return -1
2、思路二代码(49ms)
class Solution1(object):
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
if len(needle)==0:
return 0
if len(haystack)<len(needle):
return -1
def findnext(s):
a=[-1]
if len(s)==1:
return a
a.append(0)
for i in range(2,len(s)):
k=a[i-1]
while(k>-1):
if s[k]==s[i-1]:
a.append(k+1)
break
else:
k=a[k]
if k==-1:
a.append(0)
return a
next=findnext(needle)
i=0
j=0
n=len(haystack)
m=len(needle)
while i<n and j<m:
if j==-1 or haystack[i]==needle[j]:
i+=1
j+=1
elif j!=-1 and haystack[i]!=needle[j]:
j=next[j]
if j==m:
return i-j
else:
return -1
3、思路三代码(33ms):
class Solution2(object):
def strStr(self, haystack, needle):
"""
:type haystack: str
:type needle: str
:rtype: int
"""
for i in range(len(haystack)-len(needle)+1):
if haystack[i:i+len(needle)]==needle:
return i
return -1
更新
2017/11/12优化
#include <iostream>
#include <string>
#include <vector>
using namespace std;
/*
2017/11/12
KMP算法
*/
vector<int> getNext(string s)
{
vector<int>next(s.length());
next[0] = -1;
if (s.length() < 2)
return next;
next[1] = 0;
int cn = 0;
int i = 2;
while (i<s.length())
{
if (s[i - 1] == s[cn])
next[i++] = ++cn;
else if (cn>0)
cn = next[cn];
else
next[i++] = 0;
}
return next;
}
int KMP(string s1, string s2)
{
if (s1.length() < s2.length() || s1.length()==0 || s2.length()==0)
return -1;
int i1 = 0;
int i2 = 0;
vector<int>next = getNext(s2);
while (i1<s1.length() && i2<s2.length())
{
if (s1[i1] == s2[i2])
{
i1++;
i2++;
}
else if (next[i2] == -1)
i1++;
else
i2 = next[i2];
}
return i2 == s2.length() ? i1 - i2 : -1;
}
#if 1
void main()
{
string s1 = "abc123det";
string s2 = "123";
cout << KMP(s1, s2) << endl;
system("pause");
}
#else
#endif