目录
例题:字符串匹配
28. 找出字符串中第一个匹配项的下标
题干:给你两个字符串 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有什么用
KMP主要应用在字符串匹配上。
KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。
所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。
什么是前缀表
next数组就是一个前缀表(prefix table)
前缀表有什么作用呢? 前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
那么什么是前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
最长公共前后缀
文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
正确理解什么是前缀什么是后缀很重要!
最长公共前后缀,即最长相等前后缀:当前字符串 前缀和后缀 相同的最长子串的长度。
如 aabaaf,
长度为1时,next[0] 为 0;
长度为2时,next[1] 为 1,最长相同前缀后缀为 a;
长度为3时,next[2] 为 0,无最长相同前缀后缀;
长度为4时,next[3] 为 1,最长相同前缀后缀为 a;
长度为5时,next[4] 为 2,最长相同前缀后缀为 aa;
长度为6时,next[5] 为 0,无最长相同前缀后缀;
如何计算前缀表
可以看出来模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。所以要看前一位的 前缀表的数值。
构建next数组
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
private void getNext(int[] next, String s) {
int j = -1; // 初始化 next[0] = -1
next[0] = j;
// 定义两个指针i,j
// j指向前缀的末尾,i指向后缀的末尾
for (int i = 1; i < next.length; i++) {
while (j > -1 && s.charAt(j) != s.charAt(i)) { // 后缀不相等的情况
j = next[j]; // 回退
}
if (s.charAt(i) == s.charAt(j+1)) {
j++; // 相等的情况
}
next[i] = j;
}
}
使用next数组来做匹配
在文本串s里 找是否出现过模式串t。
定义两个下标j 指向模式串起始位置,i指向文本串起始位置。
那么j初始值依然为-1,为什么呢? 依然因为next数组里记录的起始位置为-1。
i就从0开始,遍历文本串,代码如下:
for (int i = 0; i < s.size(); i++)
接下来,如果s[i] != t[j+1],就从模式串中寻找下一个要匹配的位置,代码如下:
while(j > -1 && s[i] != t[j+1]) {
j = next[j];
}
如果s[i] == t[j+1],说明匹配,那么i 和 j 同时向后移动, 代码如下:
if (s[i] == t[j+1]) {
j++; // i 在for循环中+1
}
如果 判断文本串s中存在模式串t呢?当 j == 模式串的长度 t.length,说明模式串t完全匹配文本串s的一个子串,代码如下:
if (j == t.length() - 1) {
return i - j; // i - t.length() + 1
}
完整java代码:
public class LC28_strStr {
public int strStr(String haystack, String needle) {
int[] next = new int[needle.length()];
getNext(next, needle);
int j = -1;
for (int i = 0; i < haystack.length(); i++) {
while (j > -1 && haystack.charAt(i) != needle.charAt(j+1)) {
j = next[j];
}
if (haystack.charAt(i) == needle.charAt(j+1)) {
j++;
}
if (j == needle.length() - 1) {
return i - j;
}
}
return -1;
}
private void getNext(int[] next, String s) {
int j = -1; // 初始化 next[0] = -1
next[0] = j;
// 定义两个指针i,j
// j指向前缀的末尾,i指向后缀的末尾
for (int i = 1; i < next.length; i++) {
while (j > -1 && s.charAt(i) != s.charAt(j+1)) { // 后缀不相等的情况
j = next[j]; // 回退
}
if (s.charAt(i) == s.charAt(j+1)) {
j++; // 相等的情况
}
next[i] = j;
}
}
}