题目如下:
All DNA is composed of a series of nucleotides abbreviated as A, C, G, and T, for example: "ACGAATTCCG". When studying DNA, it is sometimes useful to identify repeated sequences within the DNA.
Write a function to find all the 10-letter-long sequences (substrings) that occur more than once in a DNA molecule.
For example,
Given s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT",
Return:
["AAAAACCCCC", "CCCCCAAAAA"].
分析如下:
虽然是一道在target中找寻pattern的字符串匹配类型的题目,但是有它自己的特殊点。
第一,固定了pattern的长度是10。
第二,原target中的字符只有AGCT这4种情况,所以可以把AGCT分别映射为一个数字.
'A' => 0;
'C' => 1;
'G' => 2;
'T' => 3;
这样10位的pattern就变成了一个4进制的10位数的数字。
如果两个pattern相同,则说明表达它们各自代表的数字是相同的,因此可以考虑用一个map存每次扫描过的数字,然后从头到尾扫描一遍字符串查找重复。
我的代码:
第一版
没有rolling的普通hashing,时间复杂度必然较高。O(N) * 10.
// 494ms
class Solution {
private:
map<char, int> acgt_map_;
map<long, int> count_map_;
public:
Solution (){
acgt_map_['A'] = 0;
acgt_map_['C'] = 1;
acgt_map_['G'] = 2;
acgt_map_['T'] = 3;
}
vector<string> findRepeatedDnaSequences(string s) {
vector<string> result;
if (s.length() < 11) { //NOTE1: validate input;
return result;
}
long value = 0;
for (int j = 0; j < 10; ++j) {
value = (value<<2) + acgt_map_[s[j]]; //NOTE2: right/left shif has low precedence.
}
count_map_[value] = 1;
for (int i = 1; i <= s.length() - 10; ++i) { //NOTE3: 是"小于等于",不是"小于"
value = 0;
for (int j = 0; j < 10; ++j ) {
value = (value<<2) + acgt_map_[s[i+j]];
}
if (count_map_.find(value) != count_map_.end()) {
if (count_map_[value] == 1) { //NOTE3: the two if statement can not be combined.The combination will yield different logic for else statement.
result.push_back(s.substr(i, 10));
count_map_[value]=2;
}
} else {
count_map_[value] = 1;
}
}
return result;
}
};
第二版
改进的办法是,计算hash的时候,采用rolling hashing(Karp Rabin)的思想进行计算,可以把时间复杂度简化为降低一些,虽然还是O(N) ,但是系数可以略减。
rolling hashing的思想也很容易理解:
假设 a, b ,c ,d, e , f, g , h , i, j分别依次对应0~9这10个数字。
那么任何一个10位的字符串必然对应一个唯一的数。
现在给定了一个长度为N的字符串,需要你去计算有多少个字符串匹配 abcdefghij这个pattern,那么Karp Rabin的算法是这样,采用rolling hashing的思路。
"abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij"
abcdefghij->0123456789
bcdefghija->1234567890 = (0123456789去掉最高位后再乘以10) + 0(a对应的值是0)
cdefghijab->2345678901 = (1234567890去掉最高位后再乘以10) + 1(b对应的值是1)
defghijabc->3456789012 = (2345678901去掉最高位后再乘以10) + 2(c对应的值是0)
...
使用上面这个rolling hashing的思想,在本题中,因为一共就用到了4个编码,所以可以用四进制代替十进制,而正好四进制能够很方便地使用左移右移来完成除法乘法。
得到结果如下;
//289ms
class Solution {
private:
map<char, int> acgt_map_;
map<long, int> count_map_;
public:
Solution (){
acgt_map_['A'] = 0;
acgt_map_['C'] = 1;
acgt_map_['G'] = 2;
acgt_map_['T'] = 3;
}
vector<string> findRepeatedDnaSequences(string s) {
vector<string> result;
if (s.length() < 11) {
return result; //NOTE1: validate input;
}
long value = 0;
for (int j = 0; j < 10; ++j) {
value = (value<<2) + acgt_map_[s[j]]; //NOTE2: right/left shif has low precedence.
}
count_map_[value] = 1;
std::cout<<"C: value = "<<value<<", str = "<<s.substr(0,10)<<std::endl;
for (int i = 10; i < s.length(); ++i) {
value = ((value % ((int)pow(4,9))) <<2) + acgt_map_[s[i]]; //NOTE3 : each time, value is divided by 4^9, not 4 ^10.
if (count_map_.find(value) != count_map_.end()) {
if (count_map_[value] == 1) { //NOTE4: the two if statement can not be combined.The combination will yield different logic for else statement.
result.push_back(s.substr(i - 9, 10));
count_map_[value]=2;
}
} else {
count_map_[value] = 1;
}
}
return result;
}
};
上面解法的第一个小小的优化是运行方面的。
value = ((value % ((int)pow(4,9))) <<2) + acgt_map_[s[i]];
可以写成
value = ((value % ((int)pow(4,9))) <<2) | acgt_map_[s[i]];
第二个小小的优化,只是为了让语法更简洁。 acgt_map_初始化的部分,可以写成下面的方式。(C++11表达方式)
acgt_map_ = {{'A',0}, {'C',1}, {'G',2}, {'T',3}};//C++11
第三版:
更强大的优化。
1. 用unordered_map来代替map。 在STL中,uordered_map是真正的hash表是实现的, 而map是红黑树实现的。所以unorderd_map会更加快。
2. 考虑A, C, G, T的二进制表达。
A: 1000001C: 1000011
G: 1000111
T: 1010100
可以发现其实只需要各自的最低的三位来表达就足够区分这三个数了,于是,还是采用rolling hashing的思想,只是每次都不是去计算ACGT的真正值,而是去计算ACGT的真正值的最低的3位的值。
可能还有一种想法是,如果用2位也够表示了,(即分别用0, 1, 2, 3表示A, C, G, T),但是这样还需要造一个map来存这个映射关系。如果用最低3位来表达这A, C, G, T,就连映射关系都不需要存储了。
//102ms
class Solution {
private:
unordered_map<int, int> u_map_;
public:
vector<string> findRepeatedDnaSequences(string s) {
vector<string> result;
if (s.length() < 11) {
return result; //NOTE1: validate input;
}
long value = 0;
for (int i = 0; i < s.length(); ++i) {
value = ((value<<3) | (s[i]&0x7)) & 0x3FFFFFFF;
if (i < 9) continue;
if (u_map_[value]++ == 1) {
result.push_back(s.substr(i - 9, 10));
}
}
return result;
}
};
其中,
if (u_map_[value]++ == 1)表示的意思是,先判断u_map_[value] == 1是否成立,如果成立就继续执行if(){ }内部的语句,如果不成立则跳过。然后,不管if语句是否成立,都要执行u_map_[value] ++.
如果想仔细了解这段代码的执行顺序,可以看下面的代码:
for (int i = 0; i < s.length(); ++i) {
if (u_map_[value]++ == 1) {
std::cout<<"A: i="<<i<<", u_map_[value]="<<u_map_[value]<<std::endl;
} else {
std::cout<<"B: i="<<i<<", u_map_[value]="<<u_map_[value]<<std::endl;
}
}
//输入 "AAAAAAAAAAAAA"
//输出
/*
B: i=0, u_map_[value]=1
A: i=1, u_map_[value]=2
B: i=2, u_map_[value]=3
B: i=3, u_map_[value]=4
B: i=4, u_map_[value]=5
B: i=5, u_map_[value]=6
B: i=6, u_map_[value]=7
B: i=7, u_map_[value]=8
B: i=8, u_map_[value]=9
B: i=9, u_map_[value]=10
B: i=10, u_map_[value]=11
B: i=11, u_map_[value]=12
B: i=12, u_map_[value]=13
*/
参考资料:
1 http://yuanhsh.iteye.com/blog/2185976