[路飞]重复的 DNA 序列

84 阅读3分钟

记录 1 道算法题

重复的 DNA 序列

187. 重复的DNA序列 - 力扣(LeetCode)


要求:DNA 序列由字母 A,C,G,T 组成,返回字符串中出现两次及以上的子字符串,子字符串长度为 10。比如:AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT,返回:["AAAAACCCCC","CCCCCAAAAA"]

  1. 最简单的做法就是遍历一遍字符串,10个10个的截取字符串,然后计数。
    function findRepeatedDnaSequences(s) {
        const L = 10
        const map = {}
        const res = []
        // 从第一个开始,到倒数第10个字母,依次作为开头,截取字符串
        for(let i = 0; i <= s.length - L; i++) {
            const str = s.slice(i, i + L)
            map[str] = (map[str] ?? 0) + 1
            
            if (map[str] === 2) {
                res.push(str)
            }
        }
        
        return str
    }

上面的方法时间复杂度和空间复杂度并不优秀,下面说一种更节省空间和高效的算法,采用了位运算和位掩码相关知识。

首先将 4 个字母分别记为 0,1,2,3。然后由于二进制计算中,2 (10) 和 3 (11) 占两个位置,所以一个字母提供 2 位。那么 10 个字母就占了 10 * 2 位。

接着和上面的做法一样,采用滑动窗口的方式,都是依次取 10 个字母,然后通过位掩码得出是否有相同的子序列。

 [] 滑动窗口
        a b a a a a b b b d c d e f g
     [             ] 
        a b a a a a b b b d c d e f g
       [             ]
        a b a a a a b b b d c d e f g
         [             ]
         
         ...
         
        a b a a a a b b b d c d e f g
                       [             ] 

逐行代码解析

    function findRepeatedDnaSequences(s) {
        const L = 10
        const m = {
            A: 0,
            C: 1,
            G: 2,
            T: 3
        }
        
        let x = 0
        // 由于不采用截取字符串的方式,为了代码简单,这里先计算前 9 个字母的位掩码
        for(let i = 0; i < L - 1; i++) {
            // x 先移动 2 位,这样最右边就会出现两个新位置,然后新位置和字母做或运算
            // 等价于前面的位掩码 1111001 拼上新两位 (00 | 01) 如果是 C 那么就是 01
            x = (x << 2) | m[s[i]]
        }
        
        let res = []
        let cache = new Map()
        // 上面的先计算是为了这里统一的代码,字母往后挪一位,重新计算位掩码
        for(let i = 0; i <= s.length - L; i++) {
            // 和上面的原理一样,只不过这里要多一部,就是保证 x 永远是代表 10 个字母
            // (1 << 20) - 1 得出正好是 20 位全是 1,因为是 21 位的 1000000 - 1,相当于 100 - 1 = 99 的感觉。和 20 位 111111 做与运算,这样 20 位以外的就会是 0,所以窗口限制在了 20 位的长度
            // 这里取的是滑动窗口中最后一个新加入的字母
            x = ((x << 2) | m[s[i + L - 1]]) & ((1 << 20) - 1)
            
            const count = cache.get(x) ?? 0
            cache.set(x, count + 1)
            // 如果次数是 1,这轮循环之后就会变成 2。所以当前的字符串就是出现了两次及以上
            if (count === 1) {
                // 将窗口截取出来
                res.push(s.slice(i, i + L))
            }
        }
        
        return res
    }