在问 · 算法题解

187. 重复的DNA序列

中等滑动窗口哈希表记录子串力扣原题 ↗

题目描述

DNA 序列是只由字母 'A'、'C'、'G'、'T' 组成的字符串,例如 "ACGAATTCCG"。

输入一个 DNA 序列 s。找出所有长度为 10、在 s 中出现至少两次的子串(出现位置可以重叠)。

每个这样的子串只返回一次,顺序任意。

示例 1
输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"
输出:["AAAAACCCCC","CCCCCAAAAA"]

解释:"AAAAACCCCC" 和 "CCCCCAAAAA" 都各出现了两次。

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

解释:13 个 A 中,长度为 10 的全 A 子串出现了 4 次,只返回一次。

数据范围

  • 0 <= s.length <= 10^5
  • s[i] 为 'A'、'C'、'G' 或 'T'。

解题思路:哈希表记录子串

用一个长度固定为 10 的窗口从左到右滑过 s。集合 seen 记录见过的窗口内容:如果当前子串已在 seen 中,说明它至少出现第二次,放进结果集合 res;否则加入 seen。res 也用集合,保证出现三次以上的子串只输出一次。

为什么正确

处理完起点 i 后,seen 是 s[0..i+9] 中所有长 10 的子串,res 是其中出现至少两次的子串。

复杂度分析

时间 O(10n),空间 O(10n)。若把每个字母编成 2 位,可以用滚动整数把时间降到 O(n)。

易错点

结果用列表直接追加会把出现 3 次的子串输出两次。另外循环上界是 len(s) - 9,写成 len(s) - 10 会漏掉最后一个窗口。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def findRepeatedDnaSequences(self, s: str) -> List[str]:
        seen, res = set(), set()
        for i in range(len(s) - 9):
            sub = s[i:i + 10]
            if sub in seen:
                res.add(sub)
            else:
                seen.add(sub)
        return list(res)

载入编辑器,自己跑一遍 →

其他解法

主解法每步取出长度 L=10 的子串做哈希,是 O(NL);位运算编码把子串压成一个整数并滑动更新,降到 O(N)。

解法 2:滑动窗口 + 位运算编码 时间 O(N),空间 O(N)

把 A、C、G、T 分别编码为 0–3,每个字符占 2 位,长度 10 的子串正好对应一个 20 位整数。窗口右移时,把编码左移 2 位、拼上新字符、再与 (1<<20)-1 取与去掉最早的字符,O(1) 得到新编码。哈希表以整数为键计数,计数刚好变成 2 时把该子串加入答案。

避免了每步复制和哈希长度为 10 的字符串,是滑动窗口编码的典型例子。依赖字符集只有 4 种,代码比直接存子串更难写对。

from typing import List

class Solution:
    def findRepeatedDnaSequences(self, s: str) -> List[str]:
        code = {'A': 0, 'C': 1, 'G': 2, 'T': 3}
        mask = (1 << 20) - 1
        cnt = {}
        res = []
        x = 0
        for i, ch in enumerate(s):
            x = ((x << 2) | code[ch]) & mask
            if i >= 9:
                cnt[x] = cnt.get(x, 0) + 1
                if cnt[x] == 2:
                    res.append(s[i - 9:i + 1])
        return res

滑动窗口 专题的其他题目

查看 滑动窗口 全部题目 →