徒手挖地球十九周目
NO.30 串聯所有單詞的子串 困難
這道題要把每個單詞看成整體,每個不同的單詞看作是不同的字符,單詞串就看成是特殊的字符串。
注意:s中的單詞未必是長度相等。words中可能存在相同的單詞。
思路一:暴力法 words中的單詞長度都一樣,大幅降低了這道題的難度,所以這個特點要充分利用。所以遍歷s的每個子串,分別檢查每個字串中是否符合要求。
用一個hashmap存儲words中的每個單詞及其在words中出現的次數;每遍歷一個子串都要用一個hashmap存儲被遍歷子串中出現的words中存在的單詞及其在子串中出現的次數。
重點是理解這個“要求”:1.words中的每個單詞都必須出現一次。2.words中的每個單詞必須連續出現。
反言之:檢查每個子串的過程中,出現words中的不存在的單詞則結束檢查;出現與words中相等的單詞,但是出現的次數超過其在words中出現的次數則結束檢查。
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res=new ArrayList<>();
if (words==null||words.length==0)return res;
//單詞個數、單詞長度
int wordNum = words.length,wordLen=words[0].length();
//將words每個單詞及其個數存入hashmap
HashMap<String,Integer> allWords=new HashMap<>();
for (String word : words) {
Integer value = allWords.getOrDefault(word, 0);
allWords.put(word,++value);
}
//遍歷s每一個子串,剩餘不足wordNum*wordLen個字符的子串不需要遍歷
for (int i = 0; i < s.length() - wordNum * wordLen + 1; i++) {
//將子串中出現的和words中相等的單詞及其出現次數存入hashmap
HashMap<String,Integer> hasWords=new HashMap<>();
//記錄字串中和words中相等單詞數量
int count=0;
//統計字串中連續和words中相等的單詞
while (count<wordNum){
String word = s.substring(i + count * wordLen, i + (count + 1) * wordLen);
//如果word匹配words中的單詞,就統計其出現次數
if (allWords.containsKey(word)){
Integer value = hasWords.getOrDefault(word, 0);
hasWords.put(word,++value);
//如果word出現次數超過words中這個單詞的總數量則結束統計
if (hasWords.get(word)>allWords.get(word))break;
}else {
//如果字串中出現於words中所有單詞都不匹配的word則結束統計
break;
}
//增加成功與words中匹配的單詞數量
count++;
}
if (count==wordNum)res.add(i);
}
return res;
}
時間複雜度:O(n*m) n是s長度,m是words中單詞個數。
思路二:滑動窗口優化暴力法 用循環內的map(haswords)來保存窗口中匹配的單詞,再用一個指針標記窗口當前的起始位置。
暴力方法中有幾個需要優化的地方:
-
匹配成功:
判斷i=0這個子串符合要求,如果繼續按照思路一的方法判斷。當i=3的時候,依然一次校驗每個單詞,但是“foofoo”這兩個單詞已經在i=0子串的時候校驗過了。所以暴力法中的hasword這個map並不需要每次都清空,只需要移除“bar“之後,從i=9的單詞開始判斷就好了。
-
匹配失敗,有不匹配的單詞:
判斷i=0子串時出現了“the”這個不匹配的單詞導致匹配失敗。i=3、i=6這些子串都包含“the”這個單詞,所以都不能匹配成功,所以窗口直接移動到i=9繼續校驗即可。
-
匹配失敗,單詞匹配但是數量超出:
i=0字串中“bar”出現兩次,但是words中只有一個"bar"所以匹配失敗。窗口移動到i=3,移除了“foo”但是“bar”依然多出一個,所以一定不匹配。窗口移動到i=6的時候移除了“bar”,就可以按照正常流程繼續判斷了。
不難發現,上述幾種情況的描述時,不再是每次移動一個字符,而是每次移動單詞長度。但是s中的單詞不一定都是剛好符合wordLen,如何解決這種情況?
答:分成wordLen種情況,分別進行判斷。分別從i=0開始每次移動一個單詞長度、從i=1開始每次移動一個單詞長度、從i=2開始每次移動一個單詞長度、、、直至從i=wordLen-1開始每次移動一個單詞長度。
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> result=new ArrayList<>();
if (s==null||words==null||words.length==0)return result;
int wordsNum = words.length,wordLen=words[0].length();
//將words中的單詞及其數量存入hashmap
HashMap<String,Integer> allWords=new HashMap<>();
for (String word : words) {
Integer value = allWords.getOrDefault(word, 0);
allWords.put(word,value+1);
}
//分成wordLen中情況,分別從0開始每次移動一個單詞長度~從wordLen-1開始每次移動一個單詞長度
for (int j=0;j<wordLen;j++){
//haswords存放當前子串中匹配的單詞及其個數,count當前子串匹配的單詞數量
HashMap<String,Integer> haswords=new HashMap<>();
int count=0;
//遍歷從j開始的每個子串,每次動一個單詞長度
for (int i=j;i<s.length()-wordLen*wordsNum+1;i+=wordLen){
//防止情況三出現之後,情況一繼續移除
boolean hasRemoved=false;
while (count<wordsNum){
String curWord = s.substring(i + count * wordLen, i + (count + 1) * wordLen);
//當前單詞匹配,加入haswords
if (allWords.containsKey(curWord)) {
Integer value = haswords.getOrDefault(curWord, 0);
haswords.put(curWord,value+1);
count++;
//情況三,當前單詞匹配,但是數量超了
if (haswords.get(curWord) > allWords.get(curWord)) {
hasRemoved=true;
//從i開始逐個單詞,從haswords中移除,removeNum記錄移除的單詞個數
int removeNum=0;
while (haswords.get(curWord) > allWords.get(curWord)) {
String fristWord = s.substring(i + removeNum * wordLen, i + (removeNum + 1) * wordLen);
Integer v = haswords.get(fristWord);
haswords.put(fristWord,v-1);
removeNum++;
}
//移除完畢之後,更新count
count-=removeNum;
//移動i的位置(注意removeNum要-1,因爲跳出當前循環之後,i還要+wordLen)
i+=(removeNum-1)*wordLen;
break;
}
}else{//情況二,當前單詞不匹配
//清空haswords
haswords.clear();
//i移動到當前單詞位置(因爲跳出當前循環之後,i還要+wordLen)
i+=count*wordLen;
count=0;
break;
}
}
//情況一,匹配成功
if (count==wordsNum)result.add(i);
//如果情況三沒有出現
if (count>0&&!hasRemoved){
//移除成功匹配子串的第一個元素
String fristWord = s.substring(i, i + wordLen);
Integer v = haswords.get(fristWord);
haswords.put(fristWord,v-1);
count--;
}
}
}
return result;
}
時間複雜度:O(n*wordLen) 這個時間複雜度不敢確定算的對。。。
思路二的代碼,確實非常冗雜。接受批評o(╥﹏╥)o
本人菜鳥,有錯誤請告知,感激不盡!