记录 1 道算法题
重复的 DNA 序列
要求:DNA 序列由字母 A,C,G,T 组成,返回字符串中出现两次及以上的子字符串,子字符串长度为 10。比如:AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT
,返回:["AAAAACCCCC","CCCCCAAAAA"]
。
- 最简单的做法就是遍历一遍字符串,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
}