最近刷到 LeetCode 的 187 题,重复的 DNA 序列,觉得有点意思,记录一下解题过程~
# 1. 题目
所有 DNA 都由一系列缩写为 'A'
, 'C'
, 'G'
和 'T'
的核苷酸组成,例如: "ACGAATTCCG"
。在研究 DNA 时,识别 DNA 中的重复序列有时会对研究非常有帮助。
编写一个函数来找出所有目标子串,目标子串的长度为 10,且在 DNA 字符串 s
中出现次数超过一次。
- 示例:
输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"
输出:["AAAAACCCCC","CCCCCAAAAA"]
# 2. 思路
# i. 对比首字母相同位置
一开始我想着节省效率,然后也不清楚计算 string 的 hash 会不会比较消耗性能,于是想着只记录 4 中字母的位置,按位置用 string 的 compare 函数对比相同首字符后长度为 10 的串,若和之前某个位置的字符串比对上了,则加入结果中。形成代码大致如下:
#define SEQ_SIZE 10 | |
public: | |
vector<string> findRepeatedDnaSequences(string s) | |
{ | |
vector<string> vecResult; | |
if (s.size() <= SEQ_SIZE) | |
{ | |
return vecResult; | |
} | |
unordered_map<char, vector<int>> mapChar2Poses; | |
set<string> result; | |
for (int pos = 0; pos <= s.size() - SEQ_SIZE; pos++) | |
{ | |
bool bFound = false; | |
vector<int>& vecPoses = mapChar2Poses[s[pos]]; | |
for (auto prePos : vecPoses) | |
{ | |
if (s.compare(prePos, SEQ_SIZE, s, pos, SEQ_SIZE) == 0) | |
{ | |
result.emplace(s.substr(prePos, SEQ_SIZE)); | |
bFound = true; | |
break; | |
} | |
} | |
if (bFound == false) | |
{ | |
vecPoses.emplace_back(pos); | |
} | |
} | |
for (auto& str : result) | |
{ | |
vecResult.emplace_back(str); | |
} | |
return vecResult; | |
} |
结果发现这种方式的效率很低,有个非常长输入的用例会超时,量大了以后字符串比较的效率还是非常差的。
# ii. 计算 hash
上一种直接比较字符串的方法失败后,想着利用 hash,因为题目比较的次数比我想象的多很多,计算 hash 应该能弥补回计算的损耗。然后由于这个题目的特殊性,给的字符串中只有 4 个字母,于是我利用四进制的算法去计算 hash,于是形成代码如下:
#define SEQ_SIZE 10 | |
public: | |
vector<string> findRepeatedDnaSequences(string s) | |
{ | |
vector<string> vecResult; | |
if (s.size() <= SEQ_SIZE) | |
{ | |
return vecResult; | |
} | |
unordered_map<int, int> mapHash2Count; | |
for (int index = 0; index <= s.size() - SEQ_SIZE; index++) | |
{ | |
mapHash2Count[hash(s.substr(index, SEQ_SIZE))]++; | |
} | |
for (auto& pair : mapHash2Count) | |
{ | |
if (pair.second > 1) | |
{ | |
vecResult.emplace_back(rehash(pair.first)); | |
} | |
} | |
return vecResult; | |
} | |
int hash(string str) | |
{ | |
static unordered_map<char, int> table = <!--swig0-->; | |
int result = 0; | |
int index = 0; | |
for (auto c : str) | |
{ | |
// 每一位对应 table [c] * 4 ^ index | |
result += table[c] * (1 << (2 * index)); | |
index++; | |
} | |
return result; | |
} | |
string rehash(int hashCode) | |
{ | |
static unordered_map<int, char> table = <!--swig1-->; | |
stringstream result; | |
while (hashCode != 0) | |
{ | |
result << table[hashCode % 4]; | |
hashCode = hashCode >> 2; | |
} | |
result.seekg(0, ios::end); | |
int size = result.tellg(); | |
for (; size < 10; size++) | |
{ | |
result << table[0]; | |
} | |
return result.str(); | |
} |
最终也算是过了,不过时间效率一般般:
还欠缺一些优化技巧。
# 3. 参考答案
最终找了一份优秀答案,c++ 毛毛虫(滑动窗口)+ 哈希去重:
class Solution { | |
public: | |
vector<string> findRepeatedDnaSequences(string s) { | |
unordered_set<string> once; | |
unordered_set<string> second; | |
int size = s.size(); | |
vector<string> res; | |
if (size <= 10) return res; | |
string temp = s.substr(0, 10); | |
once.insert(temp); | |
for (int i = 10; i < size; ++i){ | |
temp.erase(0, 1); | |
temp += s[i]; | |
if (once.count(temp)){ | |
second.insert(temp); | |
}else{ | |
once.insert(temp); | |
} | |
} | |
vector<string> realResult(second.begin(), second.end()); | |
return realResult; | |
} | |
}; |
效率:
和我的思路很像不过主要有两点区别:
- 使用了滑动窗口,不需要每次取子串
- 直接使用 stl 的 unordered_set 比较
- 结果保存在两个 set 中,占用的空间比我的多一点
然后我使用了滑动窗口,效率虽然有所提升但是还是不如这个答案,看来自己实现的 hash 挺难有 stl 实现的好 o (╥﹏╥) o
# 4. 总结
主要学到三点:
- 字符串直接比较省去的计算 hash 的效率,还是没有转成 hash 直接比较来的效率高
- 固定每次检查长度的遍历使用滑动窗口可以提高一定效率
- stl 的实现的效率通常还是非常高的,很多时候不需要自己去实现