leetcode187. 重复的DNA序列

传送门

题目:所有 DNA 都由一系列缩写为 A,C,G 和 T 的核苷酸组成,例如:“ACGAATTCCG”。在研究 DNA 时,识别 DNA 中的重复序列有时会对研究非常有帮助。
编写一个函数来查找目标子串,目标子串的长度为 10,且在 DNA 字符串 s 中出现次数超过一次。
输入:s = “AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT”
输出:[“AAAAACCCCC”, “CCCCCAAAAA”]

方法1. hash存遍历的每一个字串

	public List<String> findRepeatedDnaSequences(String s) {
     
        Set<String> hash = new HashSet<>();
        Set<String> ans = new HashSet<>();
        for (int i = 0; i < s.length() - 9; ++i) {
     
            String subStr = s.substring(i, i + 10);// 注意右开 是[i,i+10) ;
            if (hash.contains(subStr))
                ans.add(subStr);  // 利用set特性天然去重
            else 
                hash.add(subStr);
        }
        return new ArrayList<>(ans);
	}

方法2.字符串编码之后匹配-Rabin-Karp算法

Rabin-Karp算法介绍:

上面解法朴素字符串匹配算法会把前一次的匹配信息丢掉,然后从头再来,这样浪费资源,也增加了时间成本。由于完成两个字符串的比较需要对其中包含的字符进行逐个比较,所需的时间较长,而数值比较则一次就可以完成,那么我们首先把“搜索词”中各个字符的“码点值”通过计算,得出一个数值(这个数值必须可以表示出字符的前后顺序,而且可以随时去掉某个字符的值,可以随时添加一个新字符的值),然后对“源串”中要比较的部分进行计算,也得出一个数值,对这两个数值进行比较,就能判断字符串是否匹配。

如果我们要在 ASCII 字符集范围内查找“搜索词”,由于 ASCII 字符集中有 128 个字符,那么 M 就等于128(实际上就是出现字符的种类),比如我们要在字符串 “abcdefg” 中查找 “cde”,那么我们就可以将搜索词 “cde” 转化为
("c"的码点 * M + "d"的码点) * M + "e"的码点 = (99 * 128 + 100) * 128 + 101 = 1634917这样一个数值。

分析一下这个数值:1634917,它可以代表字符串“cde”(下面n 代表子串的长度,这里是3)

代表字符 “c” 的部分是 "c"的码点 * (M 的 n - 1 次方) = 99 * (128 的 2 次方) = 1622016
代表字符 “d” 的部分是"d"的码点 * (M 的 n - 2 次方) = 100 * (128 的 1 次方) = 12800
代表字符 “e” 的部分是 "e"的码点 * (M 的 n - 3 次方) = 101 * (128 的 0 次方) = 101
每前进一步(新的数字加入) 它之前的所有数字都要乘以一次M

我们可以随时减去其中一个字符的值,也可以随时添加一个字符的值。

“搜索词”计算好了,那么接下来计算“源串”,取“源串”的前 n 个字符(n 为“搜索词”的长度)”abc”,按照同样的方法计算其数值:

("a"的码点 * M + "b"的码点) * M + "c"的码点 = (97 * 128 + 98) * 128 + 99 = 1601891

然后将该值与“搜索词”的值进行比较即可。比较发现 1634917 与 1601891 不相等,则说明 “cde” 与 “abc” 不匹配,则继续向下寻找,

下一步应该比较 “cde” 跟 “bcd” 了,那么我们如何利用前一步的信息呢?

首先去掉 “abc” 的数值中代表 a 的部分:

(1601891 - "a"的码点 * (M 的 n - 1 次方)) = (1601891 - 97 * (1282 次方)) = 12643

然后再将结果乘以 M(这里是 128),再加上 “d” 的码点值不就成了 “bcd” 的值了吗:

12643 * 128 + "d"的码点 = 1618304 + 100 = 1618404

这样就可以继续比较 “cde” 和 “bcd” 是否匹配,以此类推。

实际上,就是大小为3的滑动窗口:
右边加进去一个新数(加上新计算出来的值),左边就就要滑出(减去左边代表的值)

具体针对本题:M是=4; 字串长是10;
注意这里滑出左边数的时候,因为先把右边新数加上了,所以导致滑出的数多乘以一次M
所以注意下面modPow不是n-1次方 而是n次方

	public List<String> findRepeatedDnaSequences(String s) {
     
        if (s.length() == 0) return new ArrayList<>();

        // 字符转换成整数表
        Map<Character, Integer> tab = 
            new HashMap<>(){
     {
     put('A', 1); put('C', 2); put('G', 3); put('T', 4);}};
        int[] nums = new int[s.length()];
        for (int i = 0; i < s.length(); ++i) nums[i] = tab.get(s.charAt(i));

        Set<Integer> hash = new HashSet<>();// 存RK值
        Set<String> ans = new HashSet<>();// 存答案字符串

        final int subStrLen = 10; // 匹配字符串长度,本题是10
        // RK值乘以的mod,本题是4(ACGT长是4); modPow是为了左边值滑出窗口时计算方便
        final int mod = 4, modPow = (int)Math.pow(mod, subStrLen);

        int RK = 0; // Rabin-Karp算法描述的编码和
        // i 是长度为10的字串的开始索引,一共有nums.length - subStrLen + 1个子串
        for (int i = 0; i < nums.length - subStrLen + 1; ++i) {
     
            if (i == 0) {
     
                for (int j = 0; j < subStrLen; ++j) {
      // 初始化RK值
                    RK = RK * mod + nums[j];
                }
            } else {
     
                // 注意这里是先加的下一个字符,此时i-1还没有被移出去,所以会多乘以mod一次
                // 所以在初始化modPow的时候 是mod的subStrLen次方,而不是subStrLen-1
				//另外, if不能放到rk更新之前
                RK = RK * mod + nums[i + subStrLen - 1] - (nums[i - 1] * modPow);
                if (hash.contains(RK)) ans.add(s.substring(i, i + 10)); 

            }
            hash.add(RK);
        }
        return new ArrayList<>(ans);
    }

你可能感兴趣的:(leetcode,字符串,leetcode,RK算法,hash)