28. 找出字符串中第一个匹配项的下标
实现 strStr() 函数。
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
示例1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
思路
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
前缀:前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
难点1:为什么能够回退重新匹配
为啥能通过获得next数组中存放的"aabaa"
的"最长相等前后缀",移动到b的原因:你已经知道了f前面的文本串与模式串字符都匹配上了那么
- 文本串不会从第二个a开始,而是可以直接从当前与f不匹配的的b开始:因为有
"aabaa"
的"最长相等前后缀"的存在,知道模式串前面一定是aa,文本串"aabaa"的相同后缀可以直接默认与模式串的相同前缀匹配过了,直接从相同前后缀的下一位开始匹配 - 模式串也不需要从第一个a开始,而是从b开始:因为有
"aabaa"
的"最长相等前后缀"的存在,知道文本串与f不配的b前面的一定是aa
j
指向前缀末尾位置,i
指向后缀末尾位置
难点2:构建next数组
构建next数组就是求模式串上的每个位置的相同前后缀
- 初始化:
int j = 0;
next[0] = j;
next[i] 表示 i(包括i)之前最长相等的前后缀长度,这里的j表示相等前缀的末尾位置
所以初始化next[0] = j
for (int i = 1; i < s.size(); i++)
i初始化为1,因为i是后缀末尾,不能包括首字符,所以从1开始
- 处理前后缀不相同的情况
如果 s[i] 与 s[j]不相等,也就是遇到前后缀末尾不相同的情况,就要向前回退,回退到next数组的前一位位置,这是个嵌套,第一个是回退到写好的next数组,而这个是用前后缀的next数组求next数组,现在我们也是匹配只不过匹配的时前缀(相当于模式串)和后缀(相当于文本串)
现在又可以分为两种情况加深了解,并用例子来加以解释:
上面是一个子串的next数组的值,可以看到这个子串的对称程度很高,所以next值都比较大
- s[i] != s[j]的前一位s[i-1]匹配时也是不相等的
此时next[i-1]为0,当s[i-1]进行匹配时j已经直接跳到开头第一个字符(j为0),不满足两个判断条件直接给next[i]赋值j(即0)
如图所示,当i=3时,j因为上一轮的比较肯定在位置0处,不满足两个判断的要求,直接给next赋值为j
- s[i] != s[j]的前一位s[i-1]匹配时是相等的
此时next[i-1]不为0,j直接会赋值为next[j-1],s[i-1]进行匹配时j++跳到的位置j正好是相同前后缀的下一位,对应的next肯定为0,接着跳到j为0的位置,然后进行s[i] == s[j]的判断
while (j > 0 && s[i] != s[j]) { // 前后缀不相同了
j = next[j-1]; // 向前回退
}
如图所示,当i=7时,j因为上一轮s[i-1]的比较肯定在相同前后缀的下一位(位置next[7-1]=3),s[7] != s[3]
j就跳到位置next[j-1]=next[3-1]=0
然后进行s[i] == s[j]的判断即s[7]与s[0]的判断,判断相等i++,next[i] = j = 1
求next数组,如果当前字符s[i] != s[j],不断回退的时候,j每次回退所在的位置一定也是和当前的s[i]不匹配的,最终j一定会回到0的位置,然后进行s[i] == s[j]的判断
- 处理前后缀相同的情况
因为next数组存的是长度,此时j和i指向相同前后缀的最后一个字符,j还要+1表示相同前后缀的长度,然后将j赋值给next[i]
if (s[i] == s[j]) { // 找到相同的前后缀
j++;
}
next[i] = j;
现在又可以分为两种情况加深了解,并用例子来加以解释:
- s[i] == s[j]的前一位s[i-1]匹配时也是相等的
只用看上一轮s[i-1]匹配时,相同前后缀的下一位和当前的s[i]是否相等,相等的话到时候的next的值肯定比上一轮大1
如图所示,i=6时,j因为上一轮s[i-1]的比较肯定在相同前后缀的下一位(位置next[6-1]=2),s[2] == s[6],j++向后移得到长度,next[6]=j
- s[i] == s[j]的前一位s[i-1]匹配时是不相等的,此时j肯定为0,因此只用比较了s[i] == s[j],next的值仅能为1
如图所示,i=4时,j因为上一轮s[i-1]的比较肯定在0处,只用比较了s[0] == s[4],next的值仅能为1
难点3:使用next数组来做匹配
在文本串s里找是否出现过模式串t。
定义两个下标
- j 指向模式串起始位置
- i指向文本串起始位置
i就从0开始,遍历文本串
for (int i = 0; i < haystack.length(); i++) {
接下来就是 s[i] 与 t[j] 进行比较。
如果 s[i] 与 t[j] 不相同,j就要从next数组里寻找下一个匹配的位置。
while (j > 0 && haystack.charAt(i) != needle.charAt(j)){// 不匹配
j = next[j - 1]; // j 寻找之前匹配的位置
}
如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下:
// 如果文本串有与模式串相等的字符
if (haystack.charAt(i) == needle.charAt(j)) {
j++; // j指向模式串,i在for里面++
}
如何判断在文本串s里出现了模式串t呢:如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。
本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。
// 当j到模式串的最后+1的时候,说明有了字串
if (j == needle.length()) {
return i - needle.length() + 1;
}
整体代码:
public class StrStr {
public int strStr(String haystack, String needle) {
// 前缀表的大小
int numNext = needle.length();
// 定义前缀表
int[] next = new int[numNext];
// 得到前缀表
getNext(next, needle);
// 使用next数组来做匹配
// 定义j指向模式串起始位置,i指向文本串起始位置
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)){// 不匹配
j = next[j - 1]; // j 寻找之前匹配的位置
}
// 如果文本串有与模式串相等的字符
if (haystack.charAt(i) == needle.charAt(j)) {
j++; // j指向模式串,i在for里面++
}
// 当j到模式串的最后+1的时候,说明有了字串
if (j == needle.length()) {
return i - needle.length() + 1;
}
}
// 没有匹配的
return -1;
}
// 定义获得前缀表不减一的next数组的方法
public void getNext(int[] next, String s) {
// 初始化
int j = 0; // 相同前缀的末尾
next[0] = j;
// i是相同后缀的末尾
for (int i = 1; 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数组存的是长度,此时j和i指向前后缀的最后一个字符,j还要+1表长度
}
next[i] = j;
}
}
}
- 时间复杂度:
O(n+m)