【数据结构与算法】之重复的DNA序列的算法求解

举报
Serendipity·y 发表于 2022/02/17 01:08:36 2022/02/17
【摘要】 一、题目要求 所有 DNA 都由一系列缩写为 ‘A’,‘C’,‘G’ 和 ‘T’ 的核苷酸组成,例如:“ACGAATTCCG”。在研究 DNA 时,识别 DNA 中的重复序列有时会对研究非常有帮助。编写...

一、题目要求

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

  
 
  • 1
  • 2
  • 示例二:
	输入:s = "AAAAAAAAAAAAA"
	输出:["AAAAAAAAAA"]

  
 
  • 1
  • 2
  • 提示:
    • 0 <= s.length <= 105
    • s[i] 为 ‘A’、‘C’、‘G’ 或 ‘T’

二、题目分析

  • 这个问题的衍生问题是解决任意长度 L 的相同问题。在这里我们将 L=10 来简化问题。
  • 我们将讨论三种不同的方法,它们都是基于滑动窗口和 hashset 的,关键是如何实现一个窗口切片。
  • 在线性时间 O(L) 内获取窗口切片很简单也比较笨。
  • 总的来说,这样回导致 O((N−L)L) 的时间消耗和巨大的空间小寒。常数时间的窗口切片 O(1) 是一个好的方法,根据实现方式可以分为两种方法:
    • Rabin-Karp 算法 = 使用旋转哈希算法实现常数窗口切片。
    • 位操作 = 使用掩码实现常数窗口切片。
  • 后面的两种方法具有 O(N−L) 的时间复杂度和适度的空间消耗,即使在长度很长的序列也是如此。

在这里插入图片描述

求解思路和算法示例

① 线性时间窗口切片 + HashSet
  • 沿长度为 N 的字符串移动长度为 L 的滑动窗口。
  • 检查滑动窗口中的序列是否在 Hashset seen 中。
    • 如果是,则找到了重复的序列,更新输出。
    • 否则,将序列添加到 HashSet seen 中。

在这里插入图片描述

  • Java 示例:
class Solution {
  public List<String> findRepeatedDnaSequences(String s) {
    int L = 10, n = s.length();
    HashSet<String> seen = new HashSet(), output = new HashSet();

    // iterate over all sequences of length L
    for (int start = 0; start < n - L + 1; ++start) {
      String tmp = s.substring(start, start + L);
      if (seen.contains(tmp)) output.add(tmp);
      seen.add(tmp);
    }
    return new ArrayList<String>(output);
  }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 复杂度分析
    • 时间复杂度:O((N−L)L)。在执行的循环中,有 N−L+1 个长度为 L 的子字符串,这会导致
      O((N−L)L) 时间复杂性。
    • 空间复杂度:使用了 O((N−L)L) 去存储 HashSet,由于 L=10 最终为时间复杂度为 O(N)。
② Rabin-Karp:使用旋转哈希实现常数时间窗口切片
  • Rabin-Karp 算法用于多模式搜索,常用于重复检测和生物信息学中寻找两个或多个蛋白质的相似性。
  • 其思想是对字符串进行切片并在滑动窗口中计算序列的哈希值,两者都是在一个常数的时间内进行的。
  • 让我们使用 AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT 作为例子。首先,将字符串转换为整数数组,如下:
    ‘A’ -> 0, ‘C’ -> 1, ‘G’ -> 2, ‘T’ -> 3;
  • 那么:
    AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT ->
    00000111110000011111100000222333
  • 计算第一个序列的哈希值:0000011111。在基数为 4 的数字系统中,该序列可视为一个数字,并散列为如下,其中c 0…4=0 和 c5…9=1 代表了 0000011111。
    在这里插入图片描述
  • 现在考虑切片 AAAAACCCCC -> AAAACCCCCA。 在整数数组中表示 0000011111 -> 0000111110,若要删除前导 0 并添加末尾 0,则重新计算哈希:
    h 1 = (h 0 × 4 − c0 4 L) + c L + 1;
  • 可以发现窗口切片和计算散列都是在常数时间内完成的。
  • 算法如下:
    • 从序列初始位置遍历序列:从 1 到 N-1。
      • 如果 start==0,计算第一个序列 s[0:L] 的哈希值。
      • 否则,从上一个哈希值计算旋转哈希。
      • 如果哈希值在 hashset 中,则找到了重复的序列,则更新输出。
      • 否则,添加到将哈希值添加到 hashset 中。
    • 返回输出列表。
  • Java 示例如下:
class Solution {
  public List<String> findRepeatedDnaSequences(String s) {
    int L = 10, n = s.length();
    if (n <= L) return new ArrayList();

    // rolling hash parameters: base a
    int a = 4, aL = (int)Math.pow(a, L);

    // convert string to array of integers
    Map<Character, Integer> toInt = new
            HashMap() {{put('A', 0); put('C', 1); put('G', 2); put('T', 3); }};
    int[] nums = new int[n];
    for(int i = 0; i < n; ++i) nums[i] = toInt.get(s.charAt(i));

    int h = 0;
    Set<Integer> seen = new HashSet();
    Set<String> output = new HashSet();
    // iterate over all sequences of length L
    for (int start = 0; start < n - L + 1; ++start) {
      // compute hash of the current sequence in O(1) time
      if (start != 0)
        h = h * a - nums[start - 1] * aL + nums[start + L - 1];
      // compute hash of the first sequence in O(L) time
      else
        for(int i = 0; i < L; ++i) h = h * a + nums[i];
      // update output and hashset of seen sequences
      if (seen.contains(h)) output.add(s.substring(start, start + L));
      seen.add(h);
    }
    return new ArrayList<String>(output);
  }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 复杂度分析
    • 时间复杂度:O(N−L)。
    • 空间复杂度:使用了 O(N−L) 存储 hashset,因为 L=10,最终为 O(N)。
③ 位操作:使用掩码实现常数时间窗口切片
  • 其思想是对字符串进行切片,并在滑动窗口中计算序列的掩码,两者都是在一个恒定的时间内进行的。
  • 和 Rabin-Karp 一样,将字符串转换为两个比特位整数数组,如下:
    A −> 0 = 002, C −> 1 = 012, G −> 2 = 102,T −> 3 = 112
  • 然后:GAAAAACCCCCAAAAACCCCCCAAAAAGGGTTT -> 200000111110000011111100000222333;
  • 计算第一个序列的掩码:200000111。序列中的每个数字(0、1、2 或 3)占用的位不超过2位:0 = 002, 1 = 012, 2 = 102, 3 = 112
  • 因此,可以在循环中计算掩码:
    • 左移以释放最后两位:bitmask <<= 2;
    • 将当前数字存储到 2000001111 的后两位:bitmask |= nums[i]。

在这里插入图片描述

  • 现在考虑切片:GAAAAACCCCC -> AAAAACCCCC。在整形数组中表示 20000011111 -> 0000011111,删除前导 2 并添加末尾 1;

在这里插入图片描述

  • 添加末尾 1 很简单,和上面的思路一样:
    • 左移以释放最后两位:bitmask <<= 2;
    • 添加 1 到最后两位:bitmask |= 1;
  • 现在的问题是删除前导 2,换句话说,问题是将 2L 位和 (2L + 1) 位设置为零。可以使用一个技巧去重置第 n 位的值:bitmask &= ~(1 << n)。即:
    • 1 << n 是设置第 n 位为 1;
    • ~(1 << n) 是设置第 n 位为 0,且全部低位为 1;
    • bitmask &= ~(1 << n) 是将 bitmask 第 n 位设置为 0;
  • 技巧的简单使用方法是先设置第 2L 位,然后再设置 (2L + 1) 位:bitmask &= ~(1 << 2 * L) & ~(1 << (2 * L + 1)。可以简化为 bitmask &= ~(3 << 2 * L):
    • 3=(11)2,因此可以设置第 2L 位和第 (2L + 1) 位为 1;
    • ~(3 << 2 * L) 会设置第 2L 位 和第 (2L + 1) 位为 0,且所有低位为 1;
    • =bitmask &= ~(3 << 2 * L) 则会将 bitmask 第 2L 和第 (2L + 1) 位设置为 0;

在这里插入图片描述

  • 可以看到窗口切片和掩码都是在一个常数时间内完成的。
  • 算法:
    • 遍历序列的起始位置:从 1 到 N−L。
    • 如果 start == 0,计算第一个序列 s[0:L] 的掩码。
    • 否则,从前一个掩码计算当前掩码。
    • 如果掩码在 hashset 中,说明是重复序列,更新输出。
    • 否则,将该掩码添加到 hashset。
    • 返回输出列表。
  • Java 示例算法:
class Solution {
  public List<String> findRepeatedDnaSequences(String s) {
    int L = 10, n = s.length();
    if (n <= L) return new ArrayList();

    // rolling hash parameters: base a
    int a = 4, aL = (int)Math.pow(a, L);

    // convert string to array of integers
    Map<Character, Integer> toInt = new
            HashMap() {{put('A', 0); put('C', 1); put('G', 2); put('T', 3); }};
    int[] nums = new int[n];
    for(int i = 0; i < n; ++i) nums[i] = toInt.get(s.charAt(i));

    int bitmask = 0;
    Set<Integer> seen = new HashSet();
    Set<String> output = new HashSet();
    // iterate over all sequences of length L
    for (int start = 0; start < n - L + 1; ++start) {
      // compute bitmask of the current sequence in O(1) time
      if (start != 0) {
        // left shift to free the last 2 bit
        bitmask <<= 2;
        // add a new 2-bits number in the last two bits
        bitmask |= nums[start + L - 1];
        // unset first two bits: 2L-bit and (2L + 1)-bit
        bitmask &= ~(3 << 2 * L);
      }
      // compute hash of the first sequence in O(L) time
      else {
        for(int i = 0; i < L; ++i) {
          bitmask <<= 2;
          bitmask |= nums[i];
        }
      }
      // update output and hashset of seen sequences
      if (seen.contains(bitmask)) output.add(s.substring(start, start + L));
      seen.add(bitmask);
    }
    return new ArrayList<String>(output);
  }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

文章来源: blog.csdn.net,作者:Serendipity·y,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/Forever_wj/article/details/111772303

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。