1.问题描述
引自:https://leetcode-cn.com/problems/implement-strstr/
这是leetcode上的一道算法题,原题如下:
实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
解这道题有两种思路:暴力匹配,KMP算法
Java源码中String.indexOf(String str)即采用暴力匹配的方式,这里不再介绍。我们主要来讲KMP算法。
2.问题分析
举个例子:在主串haystack="ABABCABABD"中查找子串needle="ABABD"第一次匹配的位置下标。
- 首先取主串和子串的第一个字符进行比较,此时i=0,j=0,H[i]==N[i],匹配成功,比较下一位。
i | 0
| A B A B C A B A B D
| A B A B D
j | 0
- 接下来发现前4位均匹配成功,在匹配第5位时,i=4,j=4,此时H[i]!=N[i],
i | 4
| A B A B C A B A B D
| A B A B D
j | 4
我们需要向右移动子串来继续匹配。通过观察发现,向右移动2位是比较合适的,原因是子串的前2位恰好能够匹配主串中i=4的前2位:
i | 4
| A B A B C A B A B D
| A B A B D
j | 2
- 接下来继续做比较,此时i=4,j=2,H[i]!=N[i],需要继续向右移动子串。通过观察发现,只能把子串移到起始位置,即向右移动2位:
i | 4
| A B A B C A B A B D
| A B A B D
j | 0
- 此时,i=4,j=0,H[i]!=N[i],主串向左移动1位:
i | 5
| A B A B C A B A B D
| A B A B D
j | 0
- 此时,i=5,j=0,H[i]==N[i],比较后续位发现均匹配,返回下标5。
在上述移动子串的过程中,我们是通过观察规律来进行移动的,现在总结一下这些规律:
- 子串"ABABD"中包含能够匹配子串起始N个字符的子子串。
比如"ABABD"的开始N[0,1]="AB",在D前面的2个字符N[2,3]="AB",即存在子子串"AB"能够匹配"ABABD"起始2个字符"AB"。
- 当主串第i个字符与子串第j个字符不匹配时,找到主串i之前的N个字符,能够与子串起始N个字符匹配,然后快速移动子串。
比如当i=4,j=4时,找到主串i=4之前的2个字符H[2,3]="AB",与子串起始2个字符N[0,1]="AB"匹配,快速移动子串使其与主串对齐。
- 两者的关联是,当主串第i个字符与子串第j个字符不匹配时,子串的前j个字符与主串i之前的j个字符是匹配的。
比如当i=4,j=4时,子串的前4个字符N[0,3]="ABAB"与主串'C'前的4个字符H[0,3]="ABAB"是相同的。
此时问题转换成,从主串找i之前的N个字符,变为从子串找j之前的N个字符,与子串起始N个字符匹配。
总结这些规律,我们发现需要一个数组,记录子串中第j个位置,能够最多匹配子串起始位置多少个字符。这就是next数组的含义。
3.next数组
首先,我们生成子串needle="ABABD"的next数组:
| A B A B D
next | 0 0 1 2 0
解释一下这个数组的含义:
next[0] = 0,这是固定的,本身没有意义。
next[1] = 0,因为N[1]=’B‘不匹配N[0]='A',所以是0。
next[2] = 1,因为N[2]=’A‘匹配N[0]='A',所以是1。
next[3] = 2,因为N[2,3]='AB'匹配N[0,1]='AB',所以是2。
next[4] = 0,因为N[2,4]='ABD'不匹配N[0,2]='ABA',且N[4]='D'不匹配N[0]='A',所以是0。
看起来很简单,再给一个示例,生成子串"AABAAAB"的next数组:
| A A B A A A B
next | 0 1 0 1 2 2 3
为什么next[5]=2呢?因为存在子子串N[4,5]=N[0,1]='AA',所以是2。
为什么next[6]=3呢?因为存在子子串N[4,6]=N[0,2]='AAB',所以是3。
看到这如果你能手动计算next[]数组的话,说明是真正理解了next[]数组的含义。同样就能理解为什么可以使用公共前后缀来计算next数组。
接下来我们要从代码实现的角度来计算next[]数组。
首先,next[0]=0,你可以理解成第1位没有前后缀,所以为0。
接下来我们要计算next[j]的值。我们先来分析下next[j]与next[j-1]之间的关系:
假设next[j-1]=0,表示N[j]之前没有连续字符匹配起始位置字符,此时只需要与N[0]比较,如果N[0]==N[j],则next[j]=1,否则next[j]=0。
假设next[j-1]=K(K>0),表示N[j]之前有K个连续字符匹配起始位置K个字符,此时分两种情况:
- 如果N[K]==N[j],则next[j]=K+1,也就是next[j]=next[j-1]+1。
- 如果N[K]!=N[j],此时N[0,K-1]==N[j-K,j-1],我们还需要计算是否存在K'(K'<=K),使得N[0,K'-1]==N[j-K'+1,j]。如果不存在,则next[j]=0。
如何计算K'的值呢?此时我们把N[j-K,j]看做一个新的子串,问题转换成求新的子串N[j-K,j]的next'数组。这个next'数组的长度为K+1,且next'[0,K-1]==next[0,K-1],想求next'[K]的值,重复一次上面的步骤即可。
我们把上述的分析转换成代码:
int[] next = new int[needle.length()];
next[0] = 0;
for (int i = 1; i < needle.length(); i++) {
int K = next[i - 1];
if (needle.charAt(i) == needle.charAt(K)) {
next[i] = K + 1;
} else {
K = K >= 1 ? next[K - 1] : 0;
if (needle.charAt(i) == needle.charAt(K)) {
next[i] = K + 1;
} else {
next[i] = 0;
}
}
}
再稍加整理优化:
int[] next = new int[needle.length()];
next[0] = 0;
for (int i = 1; i < needle.length(); i++) {
int j = next[i - 1];
while (j >= 1 && needle.charAt(i) != needle.charAt(j)) {
j = next[j - 1]
}
if (needle.charAt(i) == needle.charAt(j)) {
j++;
}
next[i] = j;
}
4.KMP算法
字符串比较的部分逻辑很简单,直接给出代码:
int i = 0;
int j = 0;
while (i < haystack.length()) {
if (haystack.charAt(i) == needle.charAt(j)) {
i++;
j++;
} else if (j >= 1) {
j = next[j - 1];
} else {
i++;
}
if (j == needle.length()) {
return i - j;
}
}