28. 实现 strStr()
给你两个字符串
haystack和needle,请你在haystack字符串中找出needle字符串的第一个匹配项的下标(下标从 0 开始)。如果needle不是haystack的一部分,则返回-1。
28. 找出字符串中第一个匹配项的下标 - 力扣(Leetcode)
思路
思路1:Sunday算法
- 给定目标字符串
haystack,模式字符串needle - 遍历
haystack,使用index标记当前位置 - 从 目标字符串中 提取 待匹配字符串与 模式串 进行匹配,待匹配字符串为
haystack中下标为[ index, index + needle.length() )的子串,- 若匹配成功,则返回
index - 否则,判断 待匹配字符串 的下一个字符
k是否包含在needle中- 若包含,则移动到匹配字符串 的下一个字符
k与needle的最后一个k相重合的位置,此时移动的步数最小,移动的步数为index = index + 偏移表[k]。 - 否则,则
index移动到k的后面。 - 原因:当前不匹配,
index需要向后移动,移动[ 1, needle.length() )步,待匹配字符串中必然都包含k,若k不在needle中,则必然会匹配失败,因此此时可以直接移动needle.length()步,令匹配字符串中不包含k这个不包含在模式串中的字符
- 若包含,则移动到匹配字符串 的下一个字符
- 若匹配成功,则返回
思路2:KMP算法
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
当不匹配部分出现时,模式串可以分为4部分,不匹配部分作为第 4 部分存在,前面已匹配部分分成最长前缀、前缀后缀之间部分和最长后缀,见图。
同样地,待匹配字符串也可以划分为这 4 部分,待匹配字符串中的前两部分(图中红色的A和B)失去成为目标字符串的可能性,由于
- 最长前缀和最长后缀相同,待匹配字符串的最长后缀(图中蓝色的C)和模式串的最长前缀(图中红色的A)相同,
- 模式串的不匹配部分(图中红色的D)和模式串的前缀后缀之间部分(图中红色的B)不相同,
- 待匹配字符串的不匹配部分(图中蓝色的D)和模式串的不匹配部分(图中红色的D)不相同,
因此,待匹配字符串的不匹配部分(图中蓝色的D)与模式串的前缀后缀之间部分(图中红色的B)可能相同,可以从这里向后继续进行匹配。
使用前缀表next存储当模式串某个字符不匹配时,回退到模式串哪个位置继续进行匹配的信息。
needle[0,i]最长公共前后缀:字符串needle下标区间为[0,i]的子串中,由其所有前缀和所有后缀构成的交集中,集合中的字符串既是前缀,也是后缀,相当于前缀和后缀的公共部分,所以称其为公共前后缀。而公共前后缀中长度最长的字符串,称为最长公共前后缀。- 前缀:不包含最后一个字符的所有以第一个字符开头的连续子串
- 后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。
next[i]:needle[0,i]的最长公共前后缀的长度。- 由上图可知,当发生字符串不匹配时,文本串的指针处于不匹配部分的位置(图中蓝色的D),我们要将模式串的指针移到前缀后缀之间的位置(图中红色的B),并从此位置进行匹配。
- 模式串前缀后缀之间的位置(图中红色的B)位于最长前缀的后面,由于数组下标从 0 算起,所以红色的 B 的第一个字符的下标为最长前缀(图中红色的A)的长度。
next[i]计算时,使用pre来标识当前最长前缀的末尾位置,而i相当于后缀的标识。pre的作用:从needle下标为 0 时遍历,前缀为[0 , pre],- 若
next[i] = next[pre]时,意味着i的位置可以形成一个与[0, pre]相同的字符串,即与[0, pre]前缀相对应的后缀,当前位置的最长公共前后缀的长度即为 字符串[0, pre]的长度,即为 。 - 若
next[i] != next[pre]时,意味着当前字符串的最长前缀不会是[0, pre],而且只会比[0, pre]短,pre要回退到字符串[0, pre-1]的最长前缀的后一个位置,即next[pre]的位置,并重复该匹配,直到找到与next[i]相等的next[pre],或者匹配位置回退到 下标 0 的位置。
next前缀表的计算步骤如下:- 初始化:
next[0]为 ,needle的匹配从下标 开始,设置pre = 0; - 在每一次循环中,一旦
needle.charAt(pre)!=needle.charAt(i)成立,就将pre回退到字符串[0 , pre-1]的最长前缀的后面,即next[pre-1]的位置 ,直到条件不成立, 或者pre == 0。 - 在回退的循环结束后,判断回退循环的条件
needle.charAt(pre)!=needle.charAt(i)是否成立,若成立,则 当前所求的next[i]为 当前字符串[0, pre]的长度,即 。这也意味着当前字符已匹配,在下一轮循环中要匹配的是下一个字符,因此令pre++。 - 若回退循环的条件不成立,则当前
pre == 0,也就是该字符与模式串needle的第一个字符也不匹配,则最长公共前后缀的长度为 0 ,设置next[i]=0即可,在下一轮的匹配要匹配的字符也是needle的第一个字符。
- 初始化:
代码
Sunday算法,代码如下:
class Solution {
public int strStr(String haystack, String needle) {
int[] map=new int[26];
// 初始化map
for(int i=0;i<map.length;i++){
map[i]=needle.length()+1;
}
// 偏移表
for(int i=0;i<needle.length();i++){
map[needle.charAt(i)-'a']=needle.length()-i;
}
for(int i=0;i<=haystack.length()-needle.length();){
if(haystack.subSequence(i,i+needle.length()).equals(needle)){
return i;
}
// 加判断,避免越界
if(i+needle.length()<haystack.length()){
i+=map[haystack.charAt(i+needle.length())-'a'];
}else{
break;
}
}
return -1;
}
}
KMP算法,代码如下:
class Solution {
public int strStr(String haystack, String needle) {
int[] next=new int[needle.length()];
getNext(needle,next);
// 指向 needle
int index=0;
for(int i=0;i<haystack.length();i++){
while(index>0 && haystack.charAt(i)!=needle.charAt(index)){
// 让needle回退 , 产生两种结果
// 1. index !=0 , 下标为 index 的字符 与 当前字符相等
// 2. index == 0 ,是否相等需要后续判断
index=next[index-1];
}
if(needle.charAt(index)==haystack.charAt(i)){
// needle.charAt(index) 与 haystack.charAt(i)匹配
// 设置 haystack串的 i+1 与 needle的 index+1 进行匹配
index++;
}
if(index==needle.length()){
// 当 index == needle.length() 时
// needle已完全匹配
return i+1-needle.length();
}
}
return -1;
}
private void getNext(String needle,int[] next){
next[0]=0;
int pre=0;
for(int i=1;i<needle.length();i++){
while(pre>0&&needle.charAt(pre)!=needle.charAt(i)){
pre=next[pre-1];
}
if(needle.charAt(i)==needle.charAt(pre)){
next[i]=++pre;
continue;
}
next[i]=pre;
}
}
}
459.重复的子字符串
给定一个非空的字符串
s,检查是否可以通过由它的一个子串重复多次构成。
思路
思路1
如果一个字符串由重复的子字符串构成,那么当两个相同的字符串拼接在一起,第一个字符串的后半部分和第二个字符串的前半部分也会构成1个相同的字符串。
因此,将两个字符串拼接在一起,然后去除新的字符串的首尾两个字符(避免对之后的判断造成干扰),然后再判断该字符串中是否仍含有此字符串。
若结果为真,则该字符串由重复的子字符串构成;否则,该字符串不是由重复的子字符串构成的。
这个方法的时间复杂度取决于判断新字符串中是否还含有原字符串,可以在此处使用 java 提供的api,也可以使用 kmp算法。
注:
StringBuilder的indexOf()方法调用了String的indexOf()方法。String的indexOf()的底层逻辑:- 先做一系列的参数的判断,确保参数有效;
- 遍历源字符串,
- 在源字符串中找到模式串第一个字符的位置,设为
m, - 逐一匹配模式串与源字符串
m起始后面对应位置的字符, - 若各字符一一匹配,则找到了结果
m,返回结果; - 否则,这次匹配失败,继续循环。
思路2
对于字符串 s: “S1 S2 S3 …… Sk-1 Sk S1 …… Sk”,由 n 个最小重复子串 "S1 S2 S3 …… Sk-1" 组成,那么对于s来说,其最长公共前后缀为 n-1 个 “S1 S2 S3 …… Sk-1 Sk”。
s的前缀需要去除 最后一个字符 S1 , 必须以 S1 开头s的后缀需要去除 第一个字符 Sk , 必须以 Sk 结尾- 当其前缀和后缀相等时,前缀中不能以第一个S1开头 ,后缀不能以最后一个Sk结尾,而前缀必须以 S1 开头,后缀必须以 Sk 结尾;因此,在
s中,从末尾从 Sk 一直删除到 S1 ,得到 由n-1个 “S1 S2 S3 …… Sk-1 Sk”组成的字符串,该字符串满足s后缀的条件,即这个字符串就是s的最长公共(相同)前后缀。 s的最长公共前后缀 比s少 1 个最小重复子串,而s的最长公共前后缀的长度就是s字符串最后一个字符的前缀表的值,即next[s.length()-1],,因此 最小重复子串的长度为s.length()-next[s.length-1]。- 如果最长公共前后缀的长度为 0 ,这意味着不存在最小重复子串,即无法通过重复子串构成字符串,因此,由多个重复子串构成的字符串满足条件:
next[s.length()-1] != 0; - 如果
s是由多个最小重复子串构成的,则s的长度是最小重复子串的长度的整数倍,即s.length() % (s.length()-next[s.length-1]) ==0。 - 通过判断上述两个条件是否成立,即可判断字符串
s是否由重复子串构成,若是,则该子串为s[0, s.length()-next[s.length-1] -1 ]。
代码
思路1,使用java提供的api 的方法,代码如下:
class Solution {
public boolean repeatedSubstringPattern(String s) {
StringBuilder sb=new StringBuilder();
sb.append(s);
sb.append(s);
sb.deleteCharAt(0);
sb.deleteCharAt(sb.length()-1);
return sb.indexOf(s)!=-1?true:false;
}
}
思路1,使用 kmp 算法的方法:
class Solution {
public boolean repeatedSubstringPattern(String s) {
StringBuilder sb=new StringBuilder();
sb.append(s);
sb.append(s);
sb.deleteCharAt(0);
sb.deleteCharAt(sb.length()-1);
return strStr(sb.toString(),s)!=-1?true:false;
}
private int strStr(String haystack, String needle) {
int[] next=new int[needle.length()];
getNext(needle,next);
// 指向 needle
int index=0;
for(int i=0;i<haystack.length();i++){
while(index>0 && haystack.charAt(i)!=needle.charAt(index)){
// 让needle回退 , 产生两种结果
// 1. index !=0 , 下标为 index 的字符 与 当前字符相等
// 2. index == 0 ,是否相等需要后续判断
index=next[index-1];
}
if(needle.charAt(index)==haystack.charAt(i)){
// needle.charAt(index) 与 haystack.charAt(i)匹配
// 设置 haystack串的 i+1 与 needle的 index+1 进行匹配
index++;
}
if(index==needle.length()){
// 当 index == needle.length() 时
// needle已完全匹配
return i+1-needle.length();
}
}
return -1;
}
private void getNext(String needle,int[] next){
next[0]=0;
int pre=0;
for(int i=1;i<needle.length();i++){
while(pre>0&&needle.charAt(pre)!=needle.charAt(i)){
pre=next[pre-1];
}
if(needle.charAt(i)==needle.charAt(pre)){
next[i]=++pre;
continue;
}
next[i]=pre;
}
}
}
思路2的代码如下:
class Solution {
public boolean repeatedSubstringPattern(String s) {
int[] next=new int[s.length()];
getNext(s,next);
int lenRepeatedStr = s.length() - next[s.length() - 1];
return next[s.length() - 1]!=0 && s.length() % lenRepeatedStr ==0 ? true : false;
}
private void getNext(String needle,int[] next){
int pre=0;
next[0]=0;
for(int i=1;i<needle.length();i++){
// 回退到 pre == 0 或 needle.charAt(i) == needle.charAt(pre)
while(pre > 0 && needle.charAt(i)!=needle.charAt(pre)){
pre=next[pre-1];
}
if(needle.charAt(i)==needle.charAt(pre)){
next[i]=++pre;
continue;
}
next[i]=pre;
}
}
}