30. 串联所有单词的子串
题目
给定一个字符串 s 和一些 长度相同 的单词 words 。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符 ,但不需要考虑 words 中单词串联的顺序。
示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
示例 3:
输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出:[6,9,12]
提示:
- 1 <= s.length <= 10^4
- s 由小写英文字母组成
- 1 <= words.length <= 5000
- 1 <= words[i].length <= 30
- words[i] 由小写英文字母组成
暴力法
class Solution { public: vector<int> findSubstring(string s, vector<string>& words) { vector<int> ret; if(s.length()==0 || words.size()==0) return ret; unordered_map<string,int>m; for(auto word:words)//映射的初始化 { if(m.find(word) == m.end()) m[word]=1; else m[word]++; } int n = s.length(); int size = words.size(); int len = words[0].size(); for(int i = 0; i<=n-size*len; i++)//遍历所有可能的起点进行判断 { unordered_map<string,int> temp; int j = i; for(; j<i+size*len; j+=len) { string word = s.substr(j, len); if(m.find(word) != m.end()) { if(temp.find(word) == temp.end()) temp[word]=1; else temp[word]++; if(temp[word] > m[word])//如果此单词出现次数超出,则i位置不合法 break; } else break; } if(j==i+size*len) ret.push_back(i); } return ret; } };
滑动窗口法
- 我们可以将起点根据当前下标与单词长度的取余结果进行分类,然后进行下面的滑动窗口(每次移动一个单词)操作,这样,我们枚举了所有起点分类的可能情况,这样可以做到不重不漏
- 在起点的所属某一个类别的情况下,由于words中所有单词的长度是相同的,所以,我们滑动一个单词,下一个起点位置还是属于这个类别,并且我们可以复用上一个阶段的窗口内的单词的计数统计信息,不用再从头开始计数,我们通过不断的滑动,可以遍历所有以属于这个类别的起点的情况,外循环又枚举了所有的起点可以属于的类,这样就可以做到不重不漏
class Solution { public List<Integer> findSubstring(String s, String[] words) { List<Integer> ret = new ArrayList<>(); int n = s.length(); int m = words.length; int w_len = words[0].length(); if(n == 0 || w_len == 0) return ret; Map<String, Integer> wordsCount = new HashMap<>(); for(String word : words) { // 将所有单词加入 HashMap,并计数 wordsCount.put(word, wordsCount.getOrDefault(word, 0) + 1); } for(int i = 0; i < w_len; ++i) { // 错位循环,保证每种情况都遍历到 Map<String, Integer> window = new HashMap<>(); int left = i; int right = i; while(right+w_len <= n && left+w_len*m <= n) { String subRight = s.substring(right, right + w_len); // 如果这个单词不在 words 中,就重置窗口 if(!wordsCount.containsKey(subRight)) { window.clear(); right += w_len; left = right; continue; } else { // 将刚进入窗口并在 words 中的单词加入窗口 Hash 表 if(window.containsKey(subRight)) window.put(subRight, window.get(subRight)+1); else window.put(subRight, 1); // 当该单词在窗口中的出现次数多于在 words 中的出现次数时, //不断删除窗口中最左边单词,直到次数相等 while(window.get(subRight) > wordsCount.get(subRight)) { String subLeft = s.substring(left, left + w_len); if(window.get(subLeft) == 1) window.remove(subLeft); else window.put(subLeft, window.get(subLeft)-1); left += w_len; } // 当窗口长度正好等于 words 总长度时,表示匹配成功,加入结果中 if(right+w_len-left == w_len*m) ret.add(left); } right += w_len; } } return ret; } };