字符串匹配算法

字符串匹配算法

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结尾截取后的字符串最长相等前后缀
0a0
1aa1
2aab0
3aaba1
4aabaa2
5aabaaf0

以数组表示为

索引012345
010120

这就是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;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值