数组题目总结 -- 滑动窗口算法延伸:字符串匹配算法(KMP,Rabin-Karp )

一. 重复的DNA序列

思路和代码:

I. 博主的做法

  • 一共 31 个用例,到第 30 个,超时了,,
  • 博主的思路就是滑动窗口
    • template:为固定的模板字符串
    • temp:从template的第二位起始的临时字符串,用于和 template 进行比较
    • 不断的遍历,如果 temp == template 并且 template 不在 res 当中,那么添加到 res 里去
class Solution {
    public List<String> findRepeatedDnaSequences(String s) {
        int len = 10;
        int left = 0;
        int start;
        int end;

        List<String> res = new ArrayList<>();

        String template = "";
        String temp = "";
        
        while(left + len <= s.length()){
            start = left + 1;
            end = start + len;

            template = s.substring(left, left + len);

            while(end <= s.length()){
                temp = s.substring(start, end);
                if(temp.equals(template) && !res.contains(template))
                    res.add(temp);
                start++;
                end = start + len;
            }
            left++;
        }


        return res;
    }
}

II. 东哥的做法

法一:hash 暴力法
  • 遍历所有含有 10 个元素的字符串,如果,有重复的,加入 res 里面
class Solution {
    public List<String> findRepeatedDnaSequences(String s) {
        Set<String> needs = new HashSet<>();
        Set<String> res = new HashSet<>();
        int len = 10;

        String temp = "";

        for(int i = 0; i + len <= s.length(); i++){
            temp = s.substring(i, i + len);

            if(needs.contains(temp))
                res.add(temp);
            else
                needs.add(temp);
        }
        return new LinkedList<>(res);
    }
}
  • 假设s的长度为 N,目标子串的长度为 L(本题 L = 10),for 循环遍历 s 的O(N)个字符,对每个字符都要截取长度为 L 的子字符串,所以这个算法的时间复杂是O(NL)。
法二:hash + 滑动窗口
  • 一旦想把字符转化成字符串,就难免需要O(L)的时间来操作。通过其他方式优化这个事:用数字代替字母
  • 把AGCT四种字符等价为0123四个数字,那么长度为L = 10的一个碱基序列其实就可以等价为一个十位数,这个数字可以唯一标识一个子串
  • 而且窗口移动的过程,其实就是给这个数字的最低位添加数字,并删除最高位数字的过程!!!
    • 可以在 O(1) 的时间完成
    • 我们再想如果用传统的10进制来算,因为字符串固定 10 位,那么我们数字也要 10 位,而10 * 10 > 2147483647 已经超过了 int 的表示范围。我们其实只用了 4 个字母,那么其他的位数就浪费了,我们最终搞一个 4 进制就 ok 了
    • 看下面一个小例子:
      • 给你输入一个字符串形式的正整数,如何把它转化成数字的形式?
	string s = "8264";
	int number = 0;
	for (int i = 0; i < s.size(); i++) {
	    // 将字符转化成数字
	    number = 10 * number + (s[i] - '0');
	    print(number);
	}
	// 打印输出:
	// 8
	// 82
	// 826
	// 8264
  • 因为我们默认的是 10 进制,接下来我们抽象一下:
	/* 在最低位添加一个数字 */
	int number = 8264;
	// number 的进制
	int R = 10;
	// 想在 number 的最低位添加的数字
	int appendVal = 3;
	// 运算,在最低位添加一位
	number = R * number + appendVal;
	// 此时 number = 82643
	
	/* 在最高位删除一个数字 */
	int number = 8264;
	// number 的进制
	int R = 10;
	// number 最高位的数字
	int removeVal = 8;
	// 此时 number 的位数
	int L = 4;
	// 运算,删除最高位数字
	number = number - removeVal * R^(L-1);
	// 此时 number = 264
  • 四进制理解不了就用十进制理解,完了,一替换就完事了
  • 理解了上面的代码,就可以看本题的代码了:
class Solution {
    public List<String> findRepeatedDnaSequences(String s) {
        int[] nums = new int[s.length()];
		//将字符串转换成数字数组
        for(int i = 0; i < s.length(); i++){
            switch(s.charAt(i)){
                case 'A':
                    nums[i] = 0;
                    break;
                case 'C':
                    nums[i] = 1;
                    break;
                case 'G':
                    nums[i] = 2;
                    break;
                case 'T':
                    nums[i] = 3;
                    break;
            }
        }
		//存放字符对应的数字(哈希)
        Set<Integer> seen = new HashSet<>();
        //存放返回结果
        Set<String> res = new HashSet<>();
        //数字的位数
        int len = 10;
        //进制
        int R = 4;
        //用于减法的参数,类比十进制:564 = 8564 - 8 * (10^3)
        int RL = (int)Math.pow(R, len - 1);

        int windowHash = 0;

        int left = 0, right = 0;

        while(right < s.length()){
        	//向右扩展窗口,增加位数	
            windowHash = windowHash * R + nums[right];
            right++;
			//当长度为 10 再进行操作
            if(right - left == len){
                if(seen.contains(windowHash))
                    res.add(s.substring(left, right));
                else
                    seen.add(windowHash);
               	//窗口收缩,左边缘右移
                windowHash = windowHash - nums[left] * RL;
                left++; 
            }

        }
        return new LinkedList<>(res);
    }
}
  • windowHash = windowHash - nums[left] * RL; left++; 这两句一定要当字符串长度为 10 的时候再做,要不然滑动窗口就没有意义,一定要在 if(right - left == len){ }里面进行操作
  • 本质上,将字符串操作转换成了数字的操作,这种想法以后可以借鉴!大大降低了时间复杂度
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

二. Rabin-Karp 算法

// Rabin-Karp 指纹字符串查找算法
int rabinKarp(String txt, String pat) {
    // 位数
    int L = pat.length();
    // 进制(只考虑 ASCII 编码)
    int R = 256;
    // 取一个比较大的素数作为求模的除数
    long Q = 1658598167;
    // R^(L - 1) 的结果
    long RL = 1;
    for (int i = 1; i <= L - 1; i++) {
        // 计算过程中不断求模,避免溢出
        RL = (RL * R) % Q;
    }
    // 计算模式串的哈希值,时间 O(L)
    long patHash = 0;
    for (int i = 0; i < pat.length(); i++) {
        patHash = (R * patHash + pat.charAt(i)) % Q;
    }
    
    // 滑动窗口中子字符串的哈希值
    long windowHash = 0;
    
    // 滑动窗口代码框架,时间 O(N)
    int left = 0, right = 0;
    while (right < txt.length()) {
        // 扩大窗口,移入字符
        windowHash = ((R * windowHash) % Q + txt.charAt(right)) % Q;
        right++;

        // 当子串的长度达到要求
        if (right - left == L) {
            // 根据哈希值判断是否匹配模式串
            if (windowHash == patHash) {
                // 当前窗口中的子串哈希值等于模式串的哈希值
                // 还需进一步确认窗口子串是否真的和模式串相同,避免哈希冲突
                if (pat.equals(txt.substring(left, right))) {
                    return left;
                }
            }
            // 缩小窗口,移出字符
            windowHash = (windowHash - (txt.charAt(left) * RL) % Q + Q) % Q;
            // X % Q == (X + Q) % Q 是一个模运算法则
            // 因为 windowHash - (txt[left] * RL) % Q 可能是负数
            // 所以额外再加一个 Q,保证 windowHash 不会是负数

            left++;
        }
    }
    // 没有找到模式串
    return -1;
}

三. KMP 算法

public class KMP {
    private int[][] dp;
    private String pat;

    public KMP(String pat) {
        this.pat = pat;
        int M = pat.length();
        // dp[状态][字符] = 下个状态
        dp = new int[M][256];
        // base case
        dp[0][pat.charAt(0)] = 1;
        // 影子状态 X 初始为 0
        int X = 0;
        // 构建状态转移图(稍改的更紧凑了)
        for (int j = 1; j < M; j++) {
            for (int c = 0; c < 256; c++)
                dp[j][c] = dp[X][c];
            dp[j][pat.charAt(j)] = j + 1;
            // 更新影子状态
            X = dp[X][pat.charAt(j)];
        }
    }

    public int search(String txt) {
        int M = pat.length();
        int N = txt.length();
        // pat 的初始态为 0
        int j = 0;
        for (int i = 0; i < N; i++) {
            // 计算 pat 的下一个状态
            j = dp[j][txt.charAt(i)];
            // 到达终止态,返回结果
            if (j == M) return i - M + 1;
        }
        // 没到达终止态,匹配失败
        return -1;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值