题目要求:
给出
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)。