【Java数据结构与算法】字符串匹配问题:Rabin-Karp算法原理与题解

Rabin-Karp算法

解决的问题

Rabin-Karp算法,它是字符串快速查找的一种算法,为了检测s是否为t的子串子串s在t中第一次出现的位置。
例如:s = “adc”;    t = “agadcef”;    s在t中第一次出现的位置就是2
如果用暴力匹配法的话时间复杂度是O(n∗m)
Rabin-Karp算法可以将字符串对比花费的时间O(m) 转化为O(1)。

算法思想

假设字符在是0-9的数字组成的,那么比较字符串 “123” 和字符串“456” 只需要比较两者的值是否相同即可知道字符串是否相同。
如果字符是小写字母, 相当于0-9的数字变成了0-25, 如果是ASCII 就是0-127
但是当数字过大,肯定会溢出,解决这个问题的办法就是取余,而取余了以后,余数不相等可以确定两个字符串一定不相等,如果余数相等则不一定相等。总结算法思想如下:

  1. 计算子串的hash值。(hash:散列函数,取模运算即为常用hash函数之一)
  2. 计算目标字符串中每个长度为【子串长度】的子串的hash值
  3. 比较hash值,如果相同这再次枚举对比每个字符, 如果不相同则必然不同直接跳过。

算法代码

例如: 在"abcde"中找”bcd“

  1. 计算”abc“的hash函数: 为:x = a ∗ 312 + b ∗ 311 + c ∗ 310
  2. x ∗ 31 + e - b ∗ 313 就计算出”bcd“的hash函数
  3. 比较hash值和目标子串的hash值,hash值相同就再比较一次字符串。
    public int rabin_karp(String src, String target) {
        if(src == null || target == null) return -1;
        int sLen = src.length(), tLen = target.length();
        if(tLen == 0) return 0;
        int Mod = 1000000009; //比较大的质数,不容易出现冲突
        int base = 31; //26个字母至少需要26进制映射,为了降低偶然性,选取质数31
        //得到最高位要乘的值
        long power = 1;
        for(int i = 0; i < tLen; i++) {
            power = (power * base) % Mod;
        }
        //计算目标子串hash值
        long targetHash = 0;
        for(int i = 0; i < tLen; i++) {
            targetHash = (targetHash * base + target.charAt(i) - 'a') % Mod;
        }
        //滑动窗口求每个长度和target相同的子串hash
        long curHash = 0;
        for(int i = 0; i < sLen; i++) {
            curHash = (curHash * base + src.charAt(i) - 'a') % Mod;
            if(i < tLen - 1) continue;
            if(i > tLen - 1){ //从第二个开始由第一个加后面的再减去最前面的
                curHash = curHash - ((src.charAt(i - tLen)  - 'a') * power) % Mod;
            }
            if(curHash < 0) curHash += Mod; //可能减成负数,就把之前mod掉的借回来
            if(curHash == targetHash) {//hash相同
                if(src.substring(i - tLen + 1, i + 1).equals(target)){
                    return i - tLen + 1;
                }
            }
        }
        return -1;
    }

Rabin-Karp算法相关题目及题解

力扣28. 实现 strStr()

题目:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置。如果不存在,则返回 -1 。


思路: 直接用Rabin-Karp算法, 时间复杂度为O(n + m);
代码:

class Solution {
    public int strStr(String haystack, String needle) {
        int tLen = needle.length(), sLen = haystack.length();
        if(tLen == 0) return 0;
        int MOD = 1000000009, base = 31;
        long power = 1;//最高位
        for(int i = 0; i < tLen; i++) {
            power = (power * base) % MOD;
        }
        //needle 的hash值
        long needleCode = 0;
        for(int i = 0; i < tLen; i++) {
            needleCode = (needleCode * base + (needle.charAt(i) - 'a')) % MOD;
        }
        long curHashCode = 0;
        for(int i = 0; i < sLen; i++) {
            curHashCode = (curHashCode * base + (haystack.charAt(i) - 'a')) % MOD;
            if(i < tLen - 1) continue;
            if(i > tLen - 1) {
                curHashCode = (curHashCode - (haystack.charAt(i - tLen) - 'a') * power) % MOD;
                if(curHashCode < 0) curHashCode += MOD; 
            }
            if(curHashCode == needleCode){
                if(haystack.substring(i - tLen + 1, i + 1).equals(needle)) {
                    return i - tLen + 1;
                }
            }
        }
        return -1;
    }
}

力扣686. 重复叠加字符串匹配

题目:给定两个字符串 a 和 b,寻找重复叠加字符串 a 的最小次数,使得字符串 b 成为叠加后的字符串 a 的子串,如果不存在则返回 -1。


思路: 同样用Rabin-Karp算法, 算当前hash值的时候循环算就好了,hash相等时,进行比较也循环比较。
需要注意的是:

  1. 要确定一个最大的重复叠加次数,用于终止循环,最大的叠加次数为:bLen / aLen + 2,即这样的情况:abc cabca
  2. hash相等,且子串相等时,返回值 (i + 1) / aLen + ((i + 1) % aLen==0?0:1)
    由于i是从0开始的。 所有当前用到的长度是 i + 1, (i + 1) / aLen刚好为整数的时候,说明叠加这么多次刚刚好在最后一个匹配完成,如果有余数,则要多一次。
class Solution {
    public int repeatedStringMatch(String a, String b) {
        if(b.equals("")) return 0;
        int aLen = a.length(), bLen = b.length();
        long power = 1;
        int MOD = 1000000009;
        int base = 31;
        long bHash = 0;
        for(int i = 0; i < bLen; i++) {
            power = (power * base) % MOD;
            bHash = (bHash * base + b.charAt(i) - 'a')%MOD;
        }
        long curHash = 0;
        int max = bLen / aLen + 2;
        for(int i = 0; i < max * aLen; i++) {
            curHash = (curHash * base + a.charAt(i % aLen) - 'a') % MOD;
            if(i < bLen - 1) continue;
            if(i > bLen - 1) {
                curHash = (curHash - (a.charAt((i - bLen) % aLen)- 'a') * power) % MOD;
            }
            if(curHash < 0) curHash += MOD;
            if(curHash == bHash) {
                if(isMatch(a, b, i - bLen + 1)) return (i + 1) / aLen + ((i + 1) % aLen==0?0:1);
            }
        }
        return -1;
    }
     public boolean isMatch(String a,String b,int k){
        //判断a从下标k开始循环是否和b匹配
        for(int i=0;i<b.length();i++){if(b.charAt(i)!=a.charAt((i+k)%a.length())){return false;}}
        return true;
    }
}

力扣.1044. 最长重复子串

题目:给你一个字符串 s ,考虑其所有 重复子串 :即:s 的连续子串,在 s 中出现 2 次或更多次。返回任意一个可能具有最长长度的重复子串

思路:
Rabin-Karp算法算出子串hash,并存起来,找到相同的hash值的子串再进行比较。确认子串相同。
①找最长的,最简单的方法是从n-1长度的子串开始找,然后找n-2长度的,但是这会很慢。
②如果长的子串满足,那么短的子串也肯定满足,即左侧都满足,右侧都不满足,所以我们可以用二分查找来找到最长满足情况的子串。
注意: hash相同再一一比较子串,比较麻烦不好实现,所以直接搞双hash进一步减少碰撞概率
代码:

class Solution {
    int MOD1 = 1000000007,MOD2 = 1000000009;
    int base1 = 31, base2 = 37;
    int end = -1;//重复子串终止位标志。
    public String longestDupSubstring(String s) {
        int l = 0, r = s.length() - 1;
        while(l < r) {
            int mid = (l + r + 1) / 2; //向上取整,左边不动自动向右靠拢
            if(hasDupString(s, mid)) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        return end == -1?"":s.substring(end - l + 1,end + 1);
    }
    /**
     * 判断长度为len的子串有没有重复的
     */
    public boolean hasDupString(String s, int len) {
        Set<Long> set1 = new HashSet<>();
        Set<Long> set2 = new HashSet<>();
        long power1 = 1, power2 = 1;//最高位权值
        for(int i = 0; i < len; i++) {
            power1 = (power1 * base1) % MOD1;
            power2 = (power2 * base2) % MOD2;
        }
        //滑动窗口算hash值
        long curHash1 = 0,curHash2 = 0;
        for(int i = 0; i < s.length(); i++) {
            curHash1 = (curHash1 * base1 + (s.charAt(i) - 'a')) % MOD1 ;
            curHash2 = (curHash2 * base2 + (s.charAt(i) - 'a')) % MOD2;
            if(i < len - 1) continue;
            if(i > len - 1) {
                curHash1 = (curHash1 - (s.charAt(i - len) - 'a') * power1) % MOD1;
                if(curHash1 < 0) curHash1 += MOD1;
                curHash2 = (curHash2 - (s.charAt(i - len) - 'a') * power2) % MOD2;
                if(curHash2 < 0) curHash2 += MOD2;
            }
            if(set1.contains(curHash1) && set2.contains(curHash2)){
                //start = i - len + 1;//起始位;
                end = i;//终止位
                return true;
            }
            set1.add(curHash1);
            set2.add(curHash2);
        }
        return false;
    }
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
串匹配是指在一个文本串查找另一个模式串的过程。常用的串匹配算法有Naïve算法、Rabin-Karp算法和Knuth-Morris-Pratt算法。 1. Naïve算法 Naïve算法是最简单的串匹配算法,也称为暴力匹配算法。它的思路是从文本串的第一个字符开始,依次比较文本串的每个字符是否与模式串的字符相等。若不相等,则继续向后比较;若相等,则比较下一个字符,直到找到完全匹配的子串或文本串被匹配完为止。 Naïve算法的时间复杂度是O(mn),其m和n分别是模式串和文本串的长度。当模式串和文本串长度相等时,最坏情况下时间复杂度达到O(n^2)。 2. Rabin-Karp算法 Rabin-Karp算法是一种基于哈希值的串匹配算法。它的思路是先将模式串和文本串都转换为哈希值,然后比较它们的哈希值是否相等。如果哈希值相等,则再逐个比较模式串和文本串的字符是否相等。这种方法可以有效地减少比较次数,提高匹配效率。 Rabin-Karp算法的时间复杂度是O(m+n),其m和n分别是模式串和文本串的长度。但是,由于哈希函数的不完全性和哈希冲突的存在,Rabin-Karp算法在某些情况下可能会出现误判。 3. Knuth-Morris-Pratt算法 Knuth-Morris-Pratt算法是一种基于前缀函数的串匹配算法。它的思路是先计算出模式串的前缀函数,然后利用前缀函数的信息来跳过已经匹配过的部分,减少比较次数。 具体来说,KMP算法在匹配过程维护一个指针i和一个指针j,其i指向文本串当前匹配的位置,j指向模式串当前匹配的位置。如果当前字符匹配成功,则i和j同时向后移动一位;如果匹配失败,则通过前缀函数计算出j需要跳转到的位置,使得前j-1个字符与文本串的对应字符已经匹配成功,然后将j指向这个位置,i不变,继续比较下一个字符。 KMP算法的时间复杂度是O(m+n),其m和n分别是模式串和文本串的长度。由于利用了前缀函数的信息,KMP算法可以在最坏情况下达到O(n)的时间复杂度,比Naïve算法和Rabin-Karp算法更加高效。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甲 烷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值