字符串匹配算法
1、KMP算法
KMP算法是模式串匹配算法,由三位学者一起发明,故将他们首字母组合命名,算法可以将字符串匹配的时间复杂度降低到O(m + n)
级别
用法:字符串匹配
时间复杂度:
O(m + n)
, 文本串加模式串的长度空间复杂度:
O(n)
next数组长度,也就是模式串长度
1.1、思想
首先我们来看一下,对于一个字符串,它的前缀是什么,后缀呢?
- 前缀:包含首字符,不包含尾字符的所有子串
- 后缀:包含尾字符,不包含首字符的所有子串
s = "aabaaf";
// 前缀 // 后缀
"a" "f"
"aa" "af"
"aab" "aaf"
"aaba" "baaf"
"aabaa" "abaaf"
KMP利用最长相等前后缀降低时间复杂度,对于字符串aabaaf
,我们构建一个以该字符结尾的字符串的最长相等前后缀的长度这样的数组
aabaaf
以索引i结尾 | 截取后的字符串 | 最长相等前后缀 |
---|---|---|
0 | a | 0 |
1 | aa | 1 |
2 | aab | 0 |
3 | aaba | 1 |
4 | aabaa | 2 |
5 | aabaaf | 0 |
以数组表示为
索引 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
值 | 0 | 1 | 0 | 1 | 2 | 0 |
这就是aabaaf
模式串的next
数组,由此我们得出这个数组只和要匹配的模式串有关,如何利用该表进行匹配呢?
拿文本串aabaabaaf
举例子,在匹配到冲突的位置时:我们就要找以不匹配前面字符结尾的最长相等前后缀,如下也就是aabaa
的数组中的值,也就是next[4] = 2
||<- 不匹配的字符位置
文本串:a a b a a b a a f
模式串:a a b a a f
next[4] = 2;
为什么呢?因为我们此时不匹配了,模式串肯定要向前移动了,因为我们知道next[4] = 2
代表什么呢? 即在[aabaa]字符串里面,前后匹配的字符串最大长度是2
,所以我们可以理解为:
这个时候,就拿前面相等的最长前后缀的下一个字符开始匹配。因为数组是从0开始的,所以长度是2的下一个正好是第三个字符,它的索引正好是2,所以这个2也可以代表模式串下一个开始匹配的元素索引,这也是为什么这个数组叫next数组
||<- 不匹配的字符位置
文本串:a a b a a b a a f
模式串:a a b a a f
-----------------------------------------
||<- 从这里开始匹配
文本串:a a b a a b a a f
模式串: a a b a a f
1.2、实现
构建next数组:
// s为模式串, 返回next数组
public int[] getNext(String s){
// 初始化数组
int N = s.length();
int[] next = new int[N];
next[0] = 0;
// j: 指向前缀末尾位置的索引, 也代表i及之前最长相等前后缀的长度
// i: 指向前缀末尾位置的索引
int j = 0, i = 1;
for(;i < N; 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;
}
kmp算法
public int kmp(String text, String s){
if(s.length() == 0) return 0;
int[] next = getNext(s);
int j = 0;
for(int i = 0; i < text.legnth(); i++) {
while(j > 0 && text.charAt(i) != s.charAt(j)) {
j = next[j - 1];
}
if(text.charAt(i) == s.charAt(j)){
j++;
}
if(j == s.length()) {
return (i - s.length() + 1);
}
}
return -1;
}
2、RK算法
RK算法是由Rabin和Karp共同提出的一个字符串匹配算法,由此得名
时间复杂度:O(m)文本串的长度
空间复杂度:O(1)
2.1、思想
先说结论,RK算法的基本思想就是:将模式串P的hash值跟主串T中的每一个长度为P的子串的hash值比较。如果不同,则它们肯定不相等;如果相同,由于哈希冲突存在,也需要按照BF算法诸位比较
如果用暴力方法,其时间复杂度为O(mn),这是因为在比较过程中,每一次比较都要遍历一遍模式串,故时间复杂度较高,如果能将每一次比较的时间复杂度将为O(1),整体性能将会大幅度改善
即:将原来的字符串比较变成整数比较
将字符串变成整数,形成一个对应关系,那就是用hash表。将key变为hashcode,假设是唯一的,如
abcde = a * 31^4 + b * 31^3 + c * 31^2 + d * 31^1 + e * 31^0 = val
,再取模。31只是一个经验值,效果较好
如何让计算这个值的操作变为O(1)呢?我们可以利用前面算好的:
我们假设文本串是abcde
,模式串为cde
,由abc
比较cde
(不成功),变为bcd
比较cde
的过程,即由abc
代表的整数,计算bcd
代表的整数的O(1)操作为:
先加, abc + d ==> abcd
(x * 31 + d) % mod (其中x为原来abc串的hash值)
再减去头,也就是 abcd - a = bcd
(x * 31 + d) % mod - a * 31^(pat.len) % mod
算的过程中避免数字过大要不断模,如果减后变为负数,再加上模即可
由于可能发生hash冲突,所以需要double check,要真正地把字符串从txt中取出来和pat比较一次,只有hashcode相等的时候,才去做这个检查,这个事情实际上很少发生
2.2、实现
class Solution {
public int strStr(String txt, String pat) {
if (pat.length() == 0) {
return 0;
}
if (txt.length() == 0) {
return -1;
}
int m = pat.length();
int MOD = 1000000;
//计算power,也就是幂,用于后面删除头用,所以要多计算一位
int power = 1;
for (int i = 0; i < m; i++) {
power = power * 31 % MOD;
}
//计算pat的hash值
int patHash = 0;
for (int i = 0; i < m; i++) {
patHash = (patHash * 31 + pat.charAt(i)) % MOD;
}
//subHash
int subStrHash = 0;
int n = txt.length();
for (int i = 0; i < n; i++) {
subStrHash = (subStrHash * 31 + txt.charAt(i)) % MOD;
//如果不够pat的长度
if (i < m - 1) {
continue;
}
//如果超过pat的长度,要去掉头
if (i >= m) {
subStrHash = subStrHash - (txt.charAt(i - m) * power) % MOD;
if (subStrHash < 0) {
subStrHash += MOD;
}
}
//double check
if (subStrHash == patHash) {
if (txt.subSequence(i - m + 1, i + 1).equals(pat)) {
return i - m + 1;
}
}
}
return -1;
}
}