字符串:KMP
题目:28. 实现 strStr()
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
输入: haystack = "hello", needle = "ll"
输出: 2
输入: haystack = "aaaaa", needle = "bba"
输出: -1
思路
BF算法(Brute Force)
将模式串和主串进行比较,一致时则继续比较下一字符,直到比较完整个模式串。不一致时将主串后移一位,模式串则从首位开始对比。
BM算法(Boyer-Moore)
利用 BF 算法,遇到不匹配字符时,每次右移一位模式串,再重新从头进行匹配,但第一次进行字符串匹配时,abcde 都匹配成功,到 x 时失败,又因为模式串每位都不相同,思考不需要再每次右移一位,而是直接跳过这些步骤。
坏字符规则
情况1
BM 算法是从后往前进行比较,发现比较的第一个字符就不匹配,将主串这个字符称之为坏字符(f) 。在发现坏字符之后,模式串 T 中查找是否含有该字符(f),若不存在 f,将模式串右移到坏字符的后面一位。
情况2
继续从右往左进行比较,发现 d 为坏字符,则需要将模式串中的 d 和坏字符对齐。
情况3:模式串中含有多个坏字符
好后缀规则
这种情况不往右移动,甚至还左移。
情况1
红色代表坏字符,绿色代表好后缀。
发现坏字符的时候此时 cac 已经匹配成功,在红色阴影处发现坏字符。此时已经匹配成功的 cac 为好后缀,拿它在模式串中查找,如果找到了另一个和好后缀相匹配的串,那就将另一个和好后缀相匹配的串 ,滑到和好后缀对齐的位置。
情况2:在模式串的头部没有发现好后缀,发现好后缀的子串也可以
总结:
- 如果模式串含有好后缀,无论是中间还是头部可以按照规则进行移动。如果好后缀在模式串中出现多次,则以最右侧的好后缀为基准
- 如果模式串头部含有好后缀子串则可以按照规则进行移动,中间部分含有好后缀子串则不可以
- 如果在模式串尾部就出现不匹配的情况,即不存在好后缀时,则根据坏字符进行移动
KMP算法(Knuth-Morris-Pratt)
KMP算法中移动位数跟主串无关,只跟模式串有关。
最长公共前后缀
移动
next 数组
在next 数组中存前缀的结尾字符下标。
next 数组的使用
next 数组实现
如果P[k] = P[q],那么next[q+1] = k+1,此时表示P[q+1]之前的子串中,存在长度为k+1的相同前后缀。
如果P[k] != P[q],说明P[q+1]之前的子串中,不会存在长度为k+1的相同前后缀。那需要去寻找长度更短的前后缀,假设长度为j,此时P[0]…P[j-1]和P[q-j]…P[q-1]依次相同。
P[0]~P[j-1]一定是以next[q]=k(即P[0]~P[k-1])为长度的那个前缀的一个最长前缀,这是必要条件。
所以P[q]和P[j]不同时,按照k = next[k]递归查找。
代码
BM算法实现
class Solution {
public int strStr(String haystack, String needle) {
//i代表主串指针,j模式串
int i,j;
//主串长度和模式串长度
int halen = haystack.length();
int nelen = needle.length();
//循环条件,这里只有 i 增长
for (i = 0 , j = 0; i < halen && j < nelen; ++i) {
//相同时,则移动 j 指针
if (haystack.charAt(i) == needle.charAt(j)) {
++j;
} else {
//不匹配时,将 j 重新指向模式串的头部,将 i 指向本次匹配的开始位置
i -= j;
j = 0;
}
}
//查询成功时返回索引,查询失败时返回 -1;
int renum = j == nelen ? i - nelen : -1;
return renum;
}
}
KMP算法实现
class Solution {
public int strStr(String haystack, String needle) {
//两种特殊情况
if (needle.length() == 0) {
return 0;
}
if (haystack.length() == 0) {
return -1;
}
// char 数组
char[] hasyarr = haystack.toCharArray();
char[] nearr = needle.toCharArray();
//长度
int halen = hasyarr.length;
int nelen = nearr.length;
//返回下标
return kmp(hasyarr,halen,nearr,nelen);
}
public int kmp (char[] hasyarr, int halen, char[] nearr, int nelen) {
//获取next 数组
int[] next = next(nearr,nelen);
int j = 0;
for (int i = 0; i < halen; ++i) {
//发现不匹配的字符,然后根据 next 数组移动指针,移动到最大公共前后缀的前缀的后一位
while (j > 0 && hasyarr[i] != nearr[j]) {
j = next[j - 1] + 1;
//超出长度时,可以直接返回不存在
if (nelen - j + i > halen) {
return -1;
}
}
//如果相同就将指针同时后移一下,比较下个字符
if (hasyarr[i] == nearr[j]) {
++j;
}
//遍历完整个模式串,返回模式串的起点下标
if (j == nelen) {
return i - nelen + 1;
}
}
return -1;
}
public int[] next (char[] needle,int len) {
//定义 next 数组
int[] next = new int[len]; //next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
// 初始化,前缀表要统一减一的操作,所以j初始化为-1
int j = -1; //j指向前缀终止位置
next[0] = j;
for(int i = 1; i < len; i++) { // 注意i从1开始,i指向后缀终止位置
while (j >= 0 && needle[i] != needle[j + 1]) { // 前后缀不相同
j = next[j]; // 向前回溯
}
if (needle[i] == needle[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
return next;
}
}