查找最长公共子串
题目要求:给定两个基因序列,对两个基因序列进行比对,找到最长的公共子串
设计理念
- 二分查找法 ;
- Hash字典 ;
- 滚动Hash计算hash值;
- 线性探查 解决冲突;
二分查找
如果考虑从1开始进行比对,这样时间复杂度将会很大,因此考虑二分法,先从n/2的长度开始比对,如果找到了,就继续查找是否有更长的子串,否则就查找更小的子串,直到到达二分查找的结束条件;
hash字典
选择用字典存储子串,key设置为h1(x1),值是一个列表;采用两个hash函数,能够降低时间复杂度;
滚动hash
计算hash值,每次需要读取一定长度的子串来进行计算,而容易知道,对于相邻的子串,有很大一部分是共有的,因此我们利用这种共性,来简化计算,每次只需要往后面读取一个字符,其余的字符利用上一个子串得到,这样我们就只需要在读取第一个字串时,读取多个字符,其余的子串只需要读取一个字符就可以了,简化了计算;如下图
这样我们就只需要每次减去前一个子串的第一个字符的权值,再加上最后一个字符的权值,就是新的子串的hash值
线性探查
如果出现了冲突,就采用最简单的线性探查,取i=1
,依次往后面进行探查,直到找到一个空的位置,这里的hash表大小刚好为子串个数,因此一定能找到一个位置。
代码部分
s1 = "SFSDFSDFJLKSJDLFKJALKJSFDJALDJF;AJSDKLFJALKSJDFJA"
s2 = "JWOIJFAFJLAJSDFOIQ;FJALDFJALKDFJSJADFLKJALSDFJLAA"
#s1 = "ABCDEFGHI"
#s2 = "SHABCIJYKL"
n = min(len(s1), len(s2))
base = ord("A") - 1
weight = 26
def create_hash_map(S, L):
"""
将字符串利用滚动哈希构造一个字典
这个字典的键是哈希函数 h1 的值,值是一个列表:[x1, h2(x1),num, key],
num表示这个键的线性探查次数,说明有多少个冲突,key存储的是发生冲突时,原来的键是多少
:param S: 基因字符串
:param L: 字串长度
:return: 哈希字典
"""
str1 = S[0:L]
D = {}
#第一个字符串
num = ord(str1[0]) - base
for i in range(1,L):
num = (num * weight + (ord(str1[i]) - base))
key = num % (n-L+1) #h1对n取模
list1 = [str1, num % ((n-L+1) * (n-L+1)), 0, key] #h2对n*n取模
D[key] = list1
#之后的字符串,滚动哈希求解散列值
for j in range(L, len(S)):
str1 += S[j]
num = weight * (num - (ord(str1[0]) - base) * pow(weight, L - 1)) + ord(str1[L]) - base
str1 = str1[1:]
#如果出现冲突,则需要线性探查,找到空的键,添加到字典当中去
key = num % (n-L+1)
list1 = [str1, num % ((n-L+1) * (n-L+1)), 0, key]
if D.get(key) is not None:
D.get(key)[2] += 1
for i in range((num % (n-L+1) + 1), 2 * (n-L+1)):
if D.get(i%(n-L+1)) is None: #找到了空的键
key = i % (n-L+1)
break
D[key] = list1
return D
def search(key1, key2, str, D, L):
"""
查找是否有相同的子串
:param key1: h1(x2)
:param key2: h2(x2)
:param str: x2
:param D: x1的字典
:return: 存在返回True,不存在返回False
"""
#第一种情况为第一次直接找到,不需要线性探查
list1 = D.get(key1)
if list1 is not None:# 如果存在 h1(x1) == h2(x2)
if list1[2] == 0:
if key2 == list1[1]: #进一步判断是否有 h2(x1) == h2(x2)
if list1[0] == str: #子字符串比较
return True
else: #如果第三个元素不等于零,说明存在冲突项,需要线性探查
count = list1[2] #线性探查次数
position = key1
while count > 0:
for i in range(position+1, (n-L+1) * 2):
list1 = D.get(i % (n-L+1))
if list1 is not None:
if list1[3] == key1: #探查到了就计数减一,并及时跳出循环,进行下一次探查
if key2 == list1[1]: #进一步判断是否有 h2(x1) == h2(x2)
if list1[0] == str: #子字符串比较
return True
position = i
count -= 1
break
return False
def find_max_substring(S, T, left, right):
L = (left + right) // 2
if L == left:
return L
#将 S 中的子字符串构造成一个哈希表,并且这个表的值是一个列表
D = create_hash_map(S, L)
# 第一个字符串的判断
str1 = T[0:L]
num = ord(str1[0]) - base
for i in range(1,L):
num = (num * weight + (ord(str1[i]) - base))
key1 = num % (n-L+1) #h1(x2)
key2 = num % ((n-L+1)*(n-L+1)) #h2(x2)
if search(key1, key2, str1, D, L): #判断是否存在相同的子串
print(str1)
return find_max_substring(S, T, L, right)
#之后的字符串进行判断
for j in range(L, len(T)):
str1 += T[j]
num = weight * (num - (ord(str1[0]) - base) * pow(weight, L - 1)) + ord(str1[L]) - base
str1 = str1[1:]
key1 = num % (n-L+1) #h1(x2)
key2 = num % ((n-L+1)*(n-L+1)) #h2(x2)
if search(key1, key2, str1, D, L): #判断是否存在相同的子串
print(str1)
return find_max_substring(S, T, L, right)
return find_max_substring(S, T, left, L)
#保证构建的hash表要小
if len(s1) <= len(s2):
print(find_max_substring(s1, s2, 0, n))
else:
print(find_max_substring(s2, s1, 0, n))
存在问题
在计算hash值的时候,我用的权重是26,当子串很长时,计算的值可能会溢出,这是需要优化的地方
这里提供一种优化思路,
可以在计算过程中,拆分进行取模运算
后言
如果有问题,欢迎在评论区指出,也欢迎各位大神提出自己的优化实现,拜谢