暴力解法(全排列+扩展KMP算法)
核心思想: 对字符串数组words[]中的元素进行排列组合,得到所有单词的全排列组合,利用扩展KMP算法对全部组合逐个进行模式匹配,将匹配成功的起始位置加入结果集。
详细说明:
全排列:参考LeetCode第四十六题(全排列)和LeetCode第四十七题(全排列II)
扩展KMP算法:实际上是利用了前缀表整体向右移动一位转化为next[]数组时被移除的那一位,可用于当needle字符串匹配成功后进行下一个起始位置匹配时需跳转的目标位置,从而实现寻找needle字符串在haystack字符串中出现的所有起始位置。
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> resList = new ArrayList<>();//串联形成的子串的起始位置的最终结果集
List<String> permuteUniqueList = permuteUniqueList(words);//获取words[]中所有单词的全排列组合
for (String permuteUnique: permuteUniqueList) {//利用扩展KMP算法对全部排列组合逐个进行模式匹配
List<Integer> startList = KMP(s, permuteUnique);//模式匹配中起始位置的结果集
resList.addAll(startList);//将起始位置的结果集中的所有元素加入最终结果集
}
return resList;
}
//利用扩展KMP算法对全部排列组合逐个进行模式匹配
public List<Integer> KMP(String haystack, String needle){
List<Integer> startList = new ArrayList<>();//结果集,用于保存needle字符串在haystack字符串中出现的所有起始位置
int[] next = new int[needle.length() + 1];//扩展KMP算法保存needle.length()+1个跳转的目标位置
next[0] = -1;
int j = 0, k = -1;
while(j < needle.length()){
if(k == -1 || needle.charAt(j) == needle.charAt(k)){
j++;
k++;
next[j] = k;
}
else
k = next[k];
}
int i = 0;
j = 0;
while(i < haystack.length() && j < needle.length()){
if(j == -1 || haystack.charAt(i) == needle.charAt(j)){
i++;
j++;
if(j == needle.length()){//匹配成功
startList.add(i - j);//将起始位置加入结果集
j = next[j];//继续匹配
}
}
else
j = next[j];
}
return startList;
}
//获取words[]中所有单词的全排列组合
public List<String> permuteUniqueList(String[] words){
List<String> resList = new ArrayList<>();
StringBuilder res = new StringBuilder();
int count = 0;
boolean[] used = new boolean[words.length];
Arrays.sort(words);
getResultUniqueList(words, resList, res, count, used);
return resList;
}
public void getResultUniqueList(String[] words, List<String> resList, StringBuilder res, int count, boolean[] used){
if(count == words.length){
resList.add(res.toString());
return;
}
for(int i = 0; i < words.length; i++){
if(used[i] || (i > 0 && words[i].equals(words[i - 1]) && !used[i - 1]))
continue;
res.append(words[i]);
used[i] = true;
getResultUniqueList(words, resList, res, count + 1, used);
res.delete(res.length() - words[0].length(), res.length());//回溯一个单词
used[i] = false;
}
}
}
滑动窗口(HashMap)
核心思想: 从字符串s中依次(每次滑动一个字符)截取与words[]中所有单词串联形成的长字符串相同长度的子串,并将此子串“切割”成若干(数量与words[]中的单词的数量一致)相同长度(长度与words[]中的单词的长度一致)的单词,判断这些单词及其个数是否和words[]中的单词及其个数一致。
注意: 判断有三种可能:(1)若字符串s的子串“切割”形成的单词及其个数与words[]中的单词及其个数一致,则将起始位置i加入结果集;(2)若字符串s的子串“切割”形成的单词不是与words[]中的单词,则跳出循环,向后滑动一个字符,从字符串s中截取下一个子串;(3)若字符串s的子串“切割”形成的单词的个数大于words[]中的单词的个数,则跳出循环,向后滑动一个字符,从字符串s中截取下一个子串。
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> resList = new ArrayList<>();//串联形成的子串的起始位置的最终结果集
Map<String,Integer> wordsMap = new HashMap<>();//用于存储words[]中的单词及其个数
for(String word : words)
wordsMap.put(word, wordsMap.getOrDefault(word, 0) + 1);
int wordLen = words[0].length();//words[]中一个单词的长度
int wordsLen = words.length * wordLen;//words[]中所有单词串联形成的长字符串的长度
//从字符串s中依次(每次滑动一个字符)截取与words[]中所有单词串联形成的长字符串相同长度的子串
for(int i = 0; i < s.length() - wordsLen + 1; i++){
String sSub = s.substring(i, i + wordsLen);//字符串s的子串
Map<String,Integer> sSubMap = new HashMap<>();//用于存储字符串s的子串“切割”形成的单词及其个数
for(int j = 0; j < wordsLen; j += wordLen){
String sSubWord = sSub.substring(j, j + wordLen);//字符串s的子串“切割”形成的单词
if(wordsMap.containsKey(sSubWord)){//字符串s的子串“切割”形成的单词是words[]中的单词
sSubMap.put(sSubWord, sSubMap.getOrDefault(sSubWord, 0) + 1);
if(sSubMap.get(sSubWord) > wordsMap.get(sSubWord))//字符串s的子串“切割”形成的单词的个数大于words[]中的单词的个数
break;
}
else
break;
}
if(sSubMap.equals(wordsMap))//字符串s的子串“切割”形成的单词及其个数与words[]中的单词及其个数一致
resList.add(i);//起始位置加入结果集
}
return resList;
}
}
滑动窗口(HashMap)-优化
核心思想: 从字符串s中依次(每次滑动一个单词)截取与words[]中所有单词串联形成的长字符串相同长度的子串,需分成wordLen种情况(其他情况都是重复),其中每种情况有三种可能(同上):(1)若字符串s的子串“切割”形成的单词及其个数与words[]中的单词及其个数一致,则将起始位置i加入结果集并向后滑动一个单词;(2)若字符串s的子串“切割”形成的单词不是与words[]中的单词,则向后滑动至该单词后一个单词位;(3)若字符串s的子串“切割”形成的单词的个数大于words[]中的单词的个数,则向后滑动至该单词第一次出现后一个单词位。
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> resList = new ArrayList<>();//串联形成的子串的起始位置的最终结果集
Map<String,Integer> wordsMap = new HashMap<>();//用于存储words[]中的单词及其个数
for(String word : words)
wordsMap.put(word, wordsMap.getOrDefault(word, 0) + 1);
int wordLen = words[0].length();//words[]中一个单词的长度
int wordsLen = words.length * wordLen;//words[]中所有单词串联形成的长字符串的长度
//分成wordLen种情况
for(int count = 0; count < wordLen; count++){
Map<String,Integer> sSubMap = new HashMap<>();//用于存储字符串s的子串“切割”形成的单词及其个数
int sSubMap_countWord = 0;//sSubMap实际存储的单词个数
//从字符串s中依次(每次滑动一个单词)截取与words[]中所有单词串联形成的长字符串相同长度的子串
for(int i = count; i < s.length() - wordsLen + 1; i += wordLen){
while(sSubMap_countWord < words.length){//每次从字符串s中截取一个单词,拼接成子串
String sWord = s.substring(i + sSubMap_countWord * wordLen, i + (sSubMap_countWord + 1) * wordLen);//从字符串s中截取的单词
if(wordsMap.containsKey(sWord)){//从字符串s中截取的单词是words[]中的单词
sSubMap.put(sWord, sSubMap.getOrDefault(sWord, 0) + 1);//加入sSubMap
if(sSubMap.get(sWord) > wordsMap.get(sWord)){//字符串s的子串“切割”形成的单词的个数大于words[]中的单词的个数
int countRemove = 0;
//向后滑动(每次滑动一个单词)至该单词第一次出现后一个单词位
while(sSubMap.get(sWord) > wordsMap.get(sWord)){
String removeWord = s.substring(i + countRemove * wordLen, i + (countRemove + 1) * wordLen);
sSubMap.put(removeWord, sSubMap.get(removeWord) - 1);
countRemove++;
}
sSubMap_countWord = sSubMap_countWord + 1 - countRemove;
i = i + (countRemove - 1) * wordLen;//向后滑动至该单词第一次出现后一个单词位(for循环中有i += wordLen)
break;
}
}
else{//从字符串s中截取的单词不是words[]中的单词
sSubMap.clear();
i = i + sSubMap_countWord * wordLen;//向后滑动至该单词后一个单词位(for循环中有i += wordLen)
sSubMap_countWord = 0;
break;
}
sSubMap_countWord++;//sSubMap实际存储的单词个数+1
}
if(sSubMap_countWord == words.length){//字符串s的子串“切割”形成的单词及其个数与words[]中的单词及其个数一致
resList.add(i);//起始位置加入结果集
String firstWord = s.substring(i, i + wordLen);
sSubMap.put(firstWord, sSubMap.get(firstWord) - 1);//向后滑动一个单词(for循环中有i += wordLen)
sSubMap_countWord--;
}
}
}
return resList;
}
}