[187].重复的 DNA 序列
Posted Debroon
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[187].重复的 DNA 序列相关的知识,希望对你有一定的参考价值。
[187].重复的 DNA 序列
题目
题目链接:https://leetcode-cn.com/problems/repeated-dna-sequences/
寻找长度 10 或以上、出现次数 2 次或以上的子串。
函数原型
vector<string> findRepeatedDnaSequences(string s)
哈希表
从头到尾扫描一次,看所有长度为 10 的子串,放入一个哈希表中,一旦发现某个长度为 10 的子串出现 2 次,这个就是返回结果。
class Solution
const int L = 10;
public:
vector<string> findRepeatedDnaSequences(string s)
vector<string> ans;
unordered_map<string, int> cnt;
int n = s.length();
for (int i = 0; i <= n - L; ++i)
string sub = s.substr(i, L); // substr 截取长期为 10 的子串
if (++cnt[sub] == 2) // 重复出现
ans.push_back(sub); // 返回结果
return ans;
;
滚动哈希
我们发现第一个子串、第二个子串之间,相同的字符有 9 个,只有 1 个字符有区别。
我们不再整体比较子串,而是看子串的哈希值,从第 1 个子串的哈希值计算出第 2 个子串的哈希值,从第 2 个子串的哈希值计算出第 3 个子串的哈希值。
因为每个字符只有 A 、 C 、 G 、 T A、C、G、T A、C、G、T 四种情况,我们用数字代替:
- A = 1 A=1 A=1
- C = 2 C=2 C=2
- G = 3 G=3 G=3
- T = 4 T=4 T=4
在程序里, m a p [ A ] = 1 map[A]=1 map[A]=1。
我们只需要保持一个长度为 10 的窗口即可,第 1 个字符删除,再最后位置添加 1 个字符。
那这个过程怎么写成计算公式?
- 添加字符: h a s h = h a s h ∗ 10 + m a p [ s [ i ] ] hash = hash * 10 + map[~s[i]~] hash=hash∗10+map[ s[i] ]
- 删除字符: h a s h = h a s h − m a p [ s [ i − 9 ] ] ∗ 1 0 9 hash = hash - map[~s[i-9]~]*10^9 hash=hash−map[ s[i−9] ]∗109
如 s[i] = 2:
- 添加字符: 123412341 ∗ 10 + 2 = 1234123412 123412341 * 10 + 2 = 1234123412 123412341∗10+2=1234123412
- 删除字符: 1234123412 − 1000000000 = 234123412 1234123412 - 1000000000=234123412 1234123412−1000000000=234123412
整个过程以此类推,这种方式叫滚动哈希。
class Solution
public:
vector<string> findRepeatedDnaSequences(string s)
vector<string> ans;
unordered_map<long, int> cnt;
if(s.length() < 10)
return ans;
int *map = new int[256];
map['A'] = 1, map['C'] = 2, map['G'] = 3, map['T'] = 4;
long hash = 0, ten9 = (long)1e9;
for(int i=0; i<9; i++) // 得到长度为 9 的子串
hash = hash * 10 + map[s[i]]; // 添加字符
for(int i=9; i<s.length(); i++) // 滚动逻辑
hash = hash * 10 + map[s[i]]; // 获取一个长度为 10 的子串
if( cnt[hash] >= 1 ) // 重复出现
ans.push_back( s.substr(i-9, 10) );
else
cnt[hash] ++;
hash = hash - map[ s[i-9] ] * ten9; // 减去最高位值,新的 9 位子串
// 发现案例 AAA···AAA 不能通过,对结果数组去重
sort(ans.begin(), ans.end());
// it 是一个迭代器,unique函数将重复出现的元素放到数组末尾
// 并返回这部分元素第一个出现的位置,最终从该位置开始删除后面所有元素即可
auto it = unique(ans.begin(), ans.end());
ans.erase(it, ans.end());
return ans;
;
Rabin-Karp 算法
滚动哈希,是一个滑动窗口 t + 哈希,窗口 t 固定长度是 10。
那么,我们就用变量 t.length 代替 10、t.length - 1 代替 9。
那之前的操作添加、删除字符也可以更改:
现在添加新字符 t.length 该怎么算呢?
首先 s[i] 不止 4 种可能,默认字符串可能任意字符组成,有 256 种可能。
其次,滑动窗口的长度可能不止 10 个了,所以为了防止整型溢出,我们得求余处理。
- 添加字符:hash = (hash * 256+s[i]) % MOD
此时,hash 是一个长度为 t.length 的字符串的哈希值。
- 删除字符:hash = hash - s[i - t.length + 1] * (B ^ (t.length - 1) ) % MOD + MOD
加 MOD 是因为,hash - s[i - t.length + 1] 有可能为负数。
比如时钟里3点向前 8 个小时,3 - 8 = -5,负数,-5 + 12(MOD)= 7,在 12 系统里,-5 就是 7。
但也有一个问题,hash - s[i - t.length + 1] 有可能为正数,后面再加一个 MOD 可能会溢出,所以,最后再一次求模。
- 删除字符:hash = (hash - s[i - t.length + 1] * (B ^ (t.length - 1) ) % MOD + MOD) % MOD
这里使用滚动哈希求解字符串匹配问题,对应的这个思路的算法,就叫 Rabin-Karp 算法。
class Solution
public:
vector<string> Rabin-Karp(string s, string t)
if(s.length() < 0) return s;
long thash = 0, MOD = (long)1e9 + 7, B = 256;
for(int i=0; i<t.length(); i++)
thash = (thash * B + s[i]) % MOD;
long hash = 0, P = 1;
for(int i=0; i<t.length()-1; i++)
P = P * B % MOD;
for(int i=0; i<t.length()-1; i++)
hash = (hash * B + s[i]) % MOD;
for(int i=t.length()-1; i<s.length(); i++)
hash = (hash * B + s[i]) % MOD;
if( hash == thash && equals(s, i-t.length()+1, t) )
return i - t.length() + 1;
hash = (hash - s[i - t.length() + 1] * P % MOD + MOD) % MOD;
return -1;
bool equals(string s, int l, string t)
for(int i=0; i<t.length(); i++)
if(t[i] != s[i+l]) return false;
return true;
;
以上是关于[187].重复的 DNA 序列的主要内容,如果未能解决你的问题,请参考以下文章
187 Repeated DNA Sequences 重复的DNA序列
[LeetCode] 187. Repeated DNA Sequences 求重复的DNA序列