LeetCode 030 Substring with Concatenation of All Words



题目要求:

给出

string s(设长度为n)

vector<string> words (设有m个string)


其中words中的string均为相同长度,要求在s中找出所有满足如下条件的子串位置:

子串为words中所有字符串以任意次序排列后拼接的结果。


注意words中所有字符串都为相同的长度(设为wordLen),所以其中每个字符串都不是其它字符串的真前缀。因此判断s中的某个子串是否满足要求就很简单,可以扫描该子串的每wordLen个字符,是否与每个words中的字符串匹配,并记录words中字符串被匹配的次数。如果被匹配的次数与实际在words中出现的次数相符的话就认为该子串是满足要求的,反之亦然。


如果words中字符串的长度不固定的话这种方法显然是不对的,因为当words中一个字符串是另一个的真前缀时,遇到s中子串出现的相同前缀时怎么办呢?例如:

s="foobarbarfoo"

words=["foo","foobar","bar"]

正确的匹配应该是s[0]..s[5]对"foobar",s[6]..s[8]对"bar",s[9]..s[11]对"foo"

可是如果优先匹配短的字符串,s[0]..s[2]与"foo"匹配,那么匹配无法进行下去。

那如果优先匹配长的字符串呢?再举个反例:

s="foobarbarfoobar"

words=["foo","foobar","barbar"]

会产生s[0]..s[5]与"foobar"匹配,匹配无法进行下去。

所以words中字符串长度固定是一个重要的性质。


那么一个初级的算法就产生了:

算法一:

枚举s中固定长度的子串的起始位置,判断每个子串是否满足要求。那么每个子串中有m个wordLen长度的子串,每wordLen个字符要与m个字符串测试是否匹配,逐字符测试是否匹配的时间复杂度是O(wordLen),所以总的时间复杂度为O(n*m*m*wordLen)。


这个时间复杂度看起来有点吓人,但是可以想到没必要和每个words中的字符串都测试是否匹配,因为我们有hashmap(unordered_map)。这就得出算法二:


算法二:

1、先把words中的m个字符串都放到hashmap中。计算每个字符串hash值的时间复杂度认为是O(wordLen)。所以这一步的时间复杂度是O(m*wordLen)。

2、枚举s中固定长度的子串的起始位置,判断每个子串是否满足要求。那么每个子串中有m个wordLen长度的子串。将每个wordLen长度的子串在hashmap中查找并计数。如果words中的字符串都消耗完的话就保存这个起始位置。时间复杂度为O(n*m*wordLen)。

所以总时间复杂度为O(n*m*wordLen)。

同时注意实现上的细节:对于hashmap的查找应该使用尽量少的次数。

C++实现以后在LeetCode上运行时间为664ms,击败49.56%的C++提交。


思考一下是什么占用时间?每次在hashmap中查找都要把子串提取出来,建立新string临时变量。同时hashmap的参数传递也会产生开销。那么我就想到了自己实现一个数据结构——字典树。字典树对查找很快,并且容易实现。当然hashmap也可以自己简单实现,但是对hash conflict处理比较麻烦。


算法三:

1、先把words中的m个字符串都放到字典树中。这一步的时间复杂度是O(m*wordLen)。

2、枚举s中固定长度的子串的起始位置,判断每个子串是否满足要求。那么每个子串中有m个wordLen长度的子串。将每个wordLen长度的子串在字典树中查找并计数。如果words中的字符串都消耗完的话就保存这个起始位置。时间复杂度为O(n*m*wordLen)。

C++实现以后在LeetCode上运行时间为68 ms,击败69.94%的C++提交。


但是还有一个地方很耗费时间:

s="aaaabbbbaaaabbbbcccc"

words=["aaaa","bbbb","cccc"]

枚举起始位置,判断每个子串是否满足要求。s中间的aaaa要在字典树中查找3次!当起始位置是0,查找一次,当起始位置是4、8时,我们已经知道中间的aaaa都是在子串里了,就没有必要再去查找它了。这就是滑动窗口思想。

窗口先在s[0]..s[7],然后滑到s[4]..s[11],最后滑到s[8]..s[15]。在滑动过程中,s中间的aaaa只需要被查找一次。

然后在枚举窗口的起始位置即可。这里窗口可以在s[0],s[1],s[2],s[3]起始。


算法四:

1、先把words中的m个字符串都放到字典树中。这一步的时间复杂度是O(m*wordLen)。

2、枚举s中固定长度的子串的起始位置对wordLen取模的值,从0到wordLen-1。然后计算滑动窗口在初始位置时,窗口里面中的起始值,即words中m’个字符串各自的出现次数(设有m‘个互相不重复的字符串),并记录满足要求的不重复的字符串个数。当窗口滑动时,对滑出的子串记录出现次数减1,对滑入的子串记录出现次数加1。如果出现次数相符变为次数不相符,记录满足要求的不重复的字符串个数减1;如果出现次数不相符变为相符,记录满足要求的不重复的字符串个数加1。当窗口滑到某个位置时,满足要求的不重复的字符串个数等于m',则保留窗口起始位置。

这么做的时间复杂度是O(n*wordLen) 。因为我们从不同的起始位置滑动了wordLen个窗口,每次s中的字符都最多被访问两次:进入窗口和退出窗口。
C++与字典树配合实现在LeetCode上的运行时间是24 ms,击败了95.75%的C++提交。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值