题目

DNA序列 由一系列核苷酸组成,缩写为 ‘A’, ‘C’, ‘G’ 和 ‘T’.。

例如,”ACGAATTCCG” 是一个 DNA序列 。
在研究 DNA 时,识别 DNA 中的重复序列非常有用。

给定一个表示 DNA序列 的字符串 s ,返回所有在 DNA 分子中出现不止一次的 长度为 10 的序列(子字符串)。你可以按 任意顺序 返回答案。

示例 1:

输入:s = “AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT”
输出:[“AAAAACCCCC”,”CCCCCAAAAA”]

示例 2:

输入:s = “AAAAAAAAAAAAA”
输出:[“AAAAAAAAAA”]

提示:

0 <= s.length <= 10^5
s[i]==’A’、’C’、’G’ or ‘T’

思路

最直接的思路是记录每一个长度为187. 重复的DNA序列 - 图1的子串的个数,时间复杂度是187. 重复的DNA序列 - 图2#card=math&code=O%28n%2Am%29&id=EO2C5),其中187. 重复的DNA序列 - 图3187. 重复的DNA序列 - 图4长度,187. 重复的DNA序列 - 图5为子串的长度,题目中为187. 重复的DNA序列 - 图6

可以优化一下,不直接记录字符串,因为比对字符串需要187. 重复的DNA序列 - 图7#card=math&code=O%28m%29&id=V7zUF)时间,而是将子串转化为哈希值,使用187. 重复的DNA序列 - 图8#card=math&code=RK%28Rabin-Karp%29&id=NE3Wn)字符串匹配算法,这里不对RK算法做详细说明了。时间复杂度为187. 重复的DNA序列 - 图9#card=math&code=O%28n%29&id=C3pvJ)。

官解的方法二很赞,使用了位运算,该方法和187. 重复的DNA序列 - 图10写法差异并不大,可以将187. 重复的DNA序列 - 图11核心的那一行代码换做位运算,变得看起来更优雅了一些。同样也是将187. 重复的DNA序列 - 图12看做四个数字,因此只需要两个比特就可以表示这四个字母,那么子串长度为187. 重复的DNA序列 - 图13就需要187. 重复的DNA序列 - 图14个比特,187. 重复的DNA序列 - 图15中的乘187. 重复的DNA序列 - 图16其实就是对应187. 重复的DNA序列 - 图17个比特左移两位,将最左面的那个字母移出去,然后将当前字母滑进来,对应位或运算,最后因为子串只有187. 重复的DNA序列 - 图18位,左边超出的要”截掉”,使用位与运算,不得不说很赞。

代码

RK算法(哈希表+滑窗)

  1. class Solution {
  2. public List<String> findRepeatedDnaSequences(String s) {
  3. int n = s.length();
  4. List<String> ans = new ArrayList<>();
  5. if (n < 10) {
  6. return ans;
  7. }
  8. // key为哈希值,value为出现次数
  9. Map<Integer, Integer> map = new HashMap<>();
  10. // 将子串转化为整数,例如s=ACGAATTCCG,char转int后为0120033112,那么其对应的val就是 0*4^9 + 1*4^8 + 2*4^7 +...+ 2*4^0
  11. int val = 0;
  12. for (int i = 0; i < 10; i++) {
  13. val = val * 4 + char2int(s.charAt(i));
  14. }
  15. map.put(val, 1);
  16. int k = (int) Math.pow(4, 9);
  17. for (int i = 10; i < n; i++) {
  18. val = (val - k * char2int(s.charAt(i - 10))) * 4 + char2int(s.charAt(i));
  19. // 只在出现第二次时添加,避免重复添加
  20. if (map.getOrDefault(val, 0) == 1) {
  21. ans.add(s.substring(i - 9, i + 1));
  22. }
  23. map.put(val, map.getOrDefault(val, 0) + 1);
  24. }
  25. return ans;
  26. }
  27. // 将ACGT四个字母转化为数字
  28. int char2int(char c) {
  29. switch (c) {
  30. case 'A':
  31. return 0;
  32. case 'C':
  33. return 1;
  34. case 'G':
  35. return 2;
  36. case 'T':
  37. return 3;
  38. }
  39. return -1;
  40. }
  41. }

哈希表+滑窗+位运算

  1. class Solution {
  2. public List<String> findRepeatedDnaSequences(String s) {
  3. int n = s.length();
  4. List<String> ans = new ArrayList<>();
  5. if (n < 10) {
  6. return ans;
  7. }
  8. Map<Integer, Integer> map = new HashMap<>();
  9. int val = 0;
  10. for (int i = 0; i < 10; i++) {
  11. // 左移两位对应RK中的乘4,位或对应RK中的+
  12. val = val << 2 | char2int(s.charAt(i));
  13. }
  14. map.put(val, 1);
  15. int k = (1 << 20) - 1;
  16. for (int i = 10; i < n; i++) {
  17. // 和(1 << 20) - 1位与是因为子串只需要20个比特,左边多出的要置0
  18. val = (val << 2 | char2int(s.charAt(i))) & k;
  19. if (map.getOrDefault(val, 0) == 1) {
  20. ans.add(s.substring(i - 9, i + 1));
  21. }
  22. map.put(val, map.getOrDefault(val, 0) + 1);
  23. }
  24. return ans;
  25. }
  26. int char2int(char c) {
  27. switch (c) {
  28. case 'A':
  29. return 0;
  30. case 'C':
  31. return 1;
  32. case 'G':
  33. return 2;
  34. case 'T':
  35. return 3;
  36. }
  37. return -1;
  38. }
  39. }