突然看见有个高手关注我,感觉非常兴奋,然后去看了下他的博客,里面有篇 LeetCode115:不同的子序列 [Python3实现]。
仔细看了看他的算法,再搜索了一下其他人的算法,发现都是在双重循环的时候直接循环到底的,但是我仔细考虑了一下,觉得似乎还可以再进一步优化一下,这样在大数据量的时候,就算节约10%的时间那也是很有数算的。
下面把其他人的算法和我进一步优化的算法都写在下面,请各位指教。
题目:
给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。
一个字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
示例 1:
输入: S = “rabbbit”, T = “rabbit”
输出: 3
解释:
如下图所示, 有 3 种可以从 S 中得到 “rabbit” 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
普通算法:
def seekStr2(mainStr, subStr):
subLen = len(subStr)
mainLen = len(mainStr)
num = [[0 for _ in range(mainLen)] for _ in range(subLen)]
if subStr[0] == mainStr[0]:
num[0][0] = 1
for i in range(1, mainLen):
if mainStr[i] == subStr[0]:
num[0][i] = num[0][i-1] + 1
else:
num[0][i] = num[0][i-1]
for i in range(1, subLen):
for j in range(i, mainLen):
if mainStr[j] == subStr[i]:
num[i][j] = num[i][j-1] + num[i - 1][j-1]
else:
num[i][j] = num[i][j-1]
number = num[subLen-1][mainLen-1]
return number
我的算法:
我仔细列表计算以后发现,其实真正使用到的是二维表中有限的数据,即以子串为行,母串为列建立的阵列中,子串的每个字母都只有在一个区间中的出现次数是有效的,超出这个区间的数都是多余的,因此计算的时候其实只需要循环该字母的有效区间即可,大于该区间以后,无论后面有没有这个字母了,其计算次数都是以有效区间的最后一次的次数为准。
所以我先用一次单循环求出每个字母的有效区间,然后在该区间循环并记录尾数以备下一个字母使用。
class Solution(object):
def numDistinct(self, mainStr, subStr):
"""
:type mainStr: str
:type subStr: str
:rtype: int
"""
if (mainStr == "" or subStr == ""):
return 0
subLen = len(subStr)
mainLen = len(mainStr)
if (mainLen < subLen):
return 0
endIndex = self.getEndIndex(mainStr, subStr)
if endIndex == []:
return 0
endNum = 0
num = [[0 for _ in range(mainLen)] for _ in range(subLen)]
if subStr[0] == mainStr[0]:
num[0][0] = 1
endNum = 1
for i in range(1, endIndex[0]):
if mainStr[i] == subStr[0]:
num[0][i] = num[0][i - 1] + 1
else:
num[0][i] = num[0][i - 1]
endNum = num[0][i]
n = 0
for i in range(1, subLen):
for j in range(i, endIndex[i]):
if mainStr[j] == subStr[i]:
if j > endIndex[i - 1]:
num[i][j] = num[i][j - 1] + endNum
else:
num[i][j] = num[i][j - 1] + num[i - 1][j - 1]
else:
num[i][j] = num[i][j - 1]
n = num[i][j]
endNum = n
return endNum
def getEndIndex(self, mainStr, subStr):
subLen = len(subStr)
mainLen = len(mainStr)
k = 1
endIndex = []
for i in range(mainLen - 1, -1, -1):
if subStr[subLen - k] == mainStr[i]:
endIndex.append(i + 1)
k += 1
if k > subLen:
break
if (subLen - k > i or i == 0):
endIndex = []
break
endIndex = endIndex[::-1]
return endIndex
这样看起来似乎比普通的算法更复杂,计算步骤更多,但是并不会多花多少时间,而且在某些情况下效率可以提高30%左右,特殊情况甚至更多。
mainStr = f"{'r'*1000}{'a'*1000}{'b'*1000}{'i'*1000}{'t'*1000}{'s'*1000}{'r'*1000}{'a'*1000}{'b'*1000}{'i'*1000}{'t'*1000}"
subStr = f"rabbits"
start = time.time()
result = seekStr2(mainStr, subStr)
end = time.time()
#for l in result:
# print(l)
print(f"共有{result}种组合,花时:{end - start}")
start = time.time()
result = seekStr3(mainStr, subStr)
end = time.time()
print(f"共有{result}种组合,花时:{end - start}")
共有499500000000000000000种组合,花时:0.012966156005859375
共有499500000000000000000种组合,花时:0.006979942321777344
今天跑去LeetCode网站看了下,发现人家真正高手的算法好简单,然后仔细分析了人家的算法,总算是明白了。
如图所示,检查母字符串中每个字母ch在子串中最后出现的位置 pos,如果pos>0则子串对应位置sta[pos] += sta[pos - 1],如果pos==0则表示是子串首字符,sta[0] += 1,然后继续在子串中向前查找,直到找不到为止。
代码如下:
class Solution2(object):
def numDistinct(self, s, t):
"""
:type s: str
:type t: str
:rtype: int
"""
n = len(t)
sta = [0] * n
for ch in s:
pos = n
while True:
pos = t.rfind(ch, 0, pos)
if pos > 0:
sta[pos] += sta[pos - 1]
elif pos == 0:
sta[pos] += 1
else:
break
return sta[n - 1]
比较我自己的算法,其实已经很接近了,可惜行百里者半九十,我把子串的每个字符在母串中确定最后出现的有效位置,却没能想一下反过来试试,果然还是懒了。开始的时候懒一下,虽然在leetcode中的记录比90%的人效率高,,但是比真正高手的效率仍然低了一半还多,事倍功半啊。看到这篇文章的同学切记切记,起初偷懒轻松后面辛苦,起初勤快吃苦后面舒服!