[187].重复的 DNA 序列
- 题目
- 函数原型
- 哈希表
- 滚动哈希
- Rabin-Karp 算法
题目
题目链接: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 个子串的哈希值。
因为每个字符只有
在程序里,。
我们只需要保持一个长度为 10 的窗口即可,第 1 个字符删除,再最后位置添加 1 个字符。
那这个过程怎么写成计算公式?
- 添加字符:
- 删除字符:
如 s[i] = 2:
- 添加字符:
- 删除字符:
整个过程以此类推,这种方式叫滚动哈希。
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;
}
};