28. 找出字符串中第一个匹配项的下标
思路:KMP
【kmp算法】最长相等前后缀,用next[]数组储存。
1. j 指向前缀末尾位,也代表包括 i 之前的子串的最长相等前后缀的长度。i 指向后缀末尾位。
2. 初始化:j = 0; next[0] = 0; i =1 ;
3. 循环不变量:遇见冲突看前一位。当前后缀不相等时,j 回退到下标为它的前一位的值的位置。当前后缀相等时,j后移,更新next数组的值。
注意:needle字符串作为模式串,因此是要获得needle字符串的next数组;
class Solution {
public int strStr(String haystack, String needle) {
if(needle.length()==0) return 0;
if(needle.length()>haystack.length()) return -1;
int[] next = getNext(needle);//获得模式串的前缀表
for(int i=0,j=0;i<haystack.length();i++){
while(j>0 && needle.charAt(j)!=haystack.charAt(i)){
j=next[j-1];//j回退
}
if(needle.charAt(j)==haystack.charAt(i)){
if(j==needle.length()-1){//匹配结束,返回i-j
return i-j;
}
j++;
}
}
return -1;
}
public int[] getNext(String s){
int[] next = new int[s.length()];
for(int i=1,j=0;i<s.length();i++){
while(j>0 && s.charAt(i)!=s.charAt(j)){//字符不匹配
j=next[j-1];
}
if(s.charAt(i)==s.charAt(j)){//字符相匹配
j++;
}
next[i]=j;//更新next[]
}
return next;
}
}
思路:基于滑动窗口的算法
1. 利用两个指针,找到文本串中第一个相匹配的字符后,依次匹配;
2. 每次遇到字符不相同的情况,文本串的查找起始位置向右移动一位,模式串从头开始;
class Solution {
public int strStr(String haystack, String needle) {
int n=needle.length(),h=haystack.length();
if(n==0) return 0;
if(n>h) return -1;
int i=0,j=0;
while(i<h-n+1){//haystack中第一个匹配的字符下标一定<h-n+1
while(i<h && haystack.charAt(i)!=needle.charAt(j)){
i++;
if(i==h-n+1) return -1;
}
while(i<h && j<n && haystack.charAt(i)==needle.charAt(j)){
i++;
j++;
}
if(j==n) return i-j;
i-=j-1;//-1:i需要回退后向右移动一步
j=0;
}
return -1;
}
}
459.重复的子字符串
思路:
【kmp算法】
1. 在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串。如图,s[0]s[1] = t[0]t[1] = k[0]k[1] = t[2]t[3] = s[2]s[3];s[0]s[1] 一定是最小重复子串。
「数组长度」-「最长相同前后缀的长度」=「一个周期的长度」
2.假设字符串s由多个重复子串(最小重复单位,长度x)构成,则s.length()=n*x;由于s的最长相同前后缀一定不包含s本身,所以最长相同前后缀长度必然是m*x,且 n-m=1。
3. 那么如果len % (len - (next[len - 1] + 1)) == 0 ,就说明该字符串有重复子字符串。「一个周期的长度」可以被「字符串长度」整除,则说明字符串由重复子串组成。
【注意】next[len-1] 若为0,则说明没有相同前后缀,返回false;
class Solution {
public boolean repeatedSubstringPattern(String s) {
int[] next = getNext(s);//得到next[]
int len = s.length();
int sub = len-next[len-1];//[一个周期的长度]
if(next[len-1]>0 && len%sub==0) return true;
return false;
}
public int[] getNext(String s){
int[] next = new int[s.length()];
for(int i=1,j=0;i<s.length();i++){
while(j>0 && s.charAt(i)!=s.charAt(j)){
j=next[j-1];
}
if(s.charAt(i)==s.charAt(j)){
j++;
}
next[i]=j;
}
return next;
}
}
字符串总结
1. 双指针法
在数组,链表和字符串中很常用。
2. 数组填充类问题
可以预先把数组扩容至填充后的大小,然后从后往前操作。
3. 固定规律一段段处理字符串
在for循环的表达式上做文章,使程序更高效。
4. KMP算法
主要思想:利用前缀表记录已匹配的文本内容,当字符串不匹配时,避免从头匹配。
用于两类经典问题:(1)匹配;(2)重复子串;
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
5. 反转的花样
例如,先整体反转,再局部反转;先局部反转,再整体反转;
双指针回顾
1. 数组篇
原地移除数组中的元素:通过两个指针在一个for循环下完成两个for循环的工作。
2. 字符串篇
原地反转字符串:双指针,分别从字符串前面、后面同时向中间移动,并交换元素;
原地填充字符串:先扩充数组替换后的大小,然后双指针从后向前进行替换;
3. 链表篇
反转链表:改变链表的next指针指向,直接将链表反转 ;
在链表中求环:定义 fast 和 slow 指针,从头结点出发,fast每次移动两节点,slow指针每次移动一节点,如果 fast 和 slow指针在途中相遇 ,则说明链表有环;分别从头结点和相遇节点出发一个指针,每次走一个节点, 这两个指针的相遇处就是环的入口;
4. N数之和篇
三数之和:通过前后两个指针不断向中间逼近,在一个for循环下完成两个for循环的工作。将暴力O(n^3)解法,降为O(n^2)的解法;四数之和的双指针解法:将暴力O(n^4)的解法,降为O(n^3)的解法。