[187].重复的 DNA 序列

Posted Debroon

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[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 ACGT 四种情况,我们用数字代替:

  • 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=hash10+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=hashmap[ s[i9] ]109

如 s[i] = 2:

  • 添加字符: 123412341 ∗ 10 + 2 = 1234123412 123412341 * 10 + 2 = 1234123412 12341234110+2=1234123412
  • 删除字符: 1234123412 − 1000000000 = 234123412 1234123412 - 1000000000=234123412 12341234121000000000=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序列

leetcode187. 重复的DNA序列

LeetCode:187. 重复的DNA序列

[LeetCode] 187. Repeated DNA Sequences 求重复的DNA序列

Leetcode No.187 重复的DNA序列(滑动窗口)

Leetcode No.187 重复的DNA序列(滑动窗口)