翻译自N00tc0d3r(请自行翻墙)

学习下技术文章到底应该怎么写,对比于百度搜索出来的各种解题思路,我只能呵呵!

题目:

​ Substring with Concatenation of All Words

	You are given a string, S, and a list of words, L, that are all of the same length. Find all starting indices of substring(s) in S that is a concatenation of each word in L exactly once and without any intervening characters.

	For example, given:
	S: "barfoothefoobarman"
	L: ["foo", "bar"]

	You should return the indices: [0,9].
	(order does not matter).
	
	举例:给定一个字符串S,和一个包含m个单词的词组L,每个单词长度为k,如果字符串S里包含由词组L中所有单词组成的字符串子串,返回该子串的起始位置

解题思路:

基本想法是遍历字符串S的每个字符,判断当前字符是否是词组L中某个单词的开头

###怎么判断当前字符是某个单词的开头?

因为我们知道词组L中的所有单词都是固定长度的,所以从字符串S的结点i开始,我们将字符串S截取成m段字符串子串(后面都直接叫子串),这里m是词组L中单词的总数,然后我们就可以判断每个子串是否是词组L中的单词和是否重复

###怎么判断每个子串是集合L中的单词?

最简单的方法是拿子串和词组L中的每个单词比较。但是对于每个子串,时间复杂度是O(m),当我们有特别多的子串或者词组中单词总数特别多时,时间开销代价特别高。实际上的时间复杂度是O(k*m) = O(m),这里k是单词的长度。这意味着如果单词的长度不是特别短的话,时间开销的代价会更高。

我们可以将词组L的单词存储在一个hashTable里,而hashTable搜索的时间复杂度是O(1)

什么样的hashTable?怎么判断重复子串?如果词组L本身包含重复单词?

我们创建了一个包含词组L中所有单词的hashTable。每当有子串匹配上单词时,我们将该单词从这个hashTable移除。我们使用HashMap<String,Integer>来替代HashMap<String,Boolean>以便于统计词组L中每个单词的出现次数。因此每当有子串匹配上单词时,我们将HashMap中该词组的次数减一直到次数为零时,我们将该单词从HashMap中移除。

现在我们有了一个大体上的思路:

  • 创建一个包含词组L中所有单词和出现次数的hashTable
  • 对于字符串S中的每个字符,判断是否是某个单词的组成部分。如果满足以下的所有条件,将字符的位置记录到结果list里。

    • 后续所有长度为k的子串都是词组L中的单词。
    • 词组L中的每个单词仅出现一次。

这个算法的复杂度是什么?

判断每个字符的时间复杂度是O(k*M)=O(m),我们需要判断(n-k*m)次,所以总的时间复杂度是O((n-k*m)*m)。因为我们创建了一个hashTable,所以空间复杂度是取决于词组L的单词总数,也就是O(k*m) = O(m).

public ArrayList<Integer> findSubstring(String S, String[] L) {  
  
  			ArrayList<Integer> indices = new ArrayList<Integer>();  
        if (L.length == 0) return indices;  

        int total = L.length, wordLen = L[0].length();  

        // store the words and frequencies in a hash table  
        HashMap<String, Integer> words = new HashMap<String, Integer>();  
        for (String s : L) {  
          if (words.containsKey(s)) {  
              words.put(s, words.get(s)+1);  
          } else {  
              words.put(s, 1);  
          }  
        }  

        // find concatenations  
        for (int i=0; i <= S.length() - total*wordLen; ++i) {  
           // check if it is a concatenation   
          HashMap<String, Integer> target = new HashMap<String, Integer>(words);  
          for (int j = i; j <= S.length() - wordLen && !target.isEmpty(); j+=wordLen) {  
              String sub = S.substring(j, j+wordLen);  
              if (!target.containsKey(sub)) break;  
              if (target.get(sub) > 1) {  // reduce the frequency
                target.put(sub, target.get(sub)-1);  
              } else {  // remove the word if only one left
                target.remove(sub);  
              }
          }  
          if (target.isEmpty()) {  
              indices.add(i);  
          }  
        }  

      	return indices; 
}  
 	

观察上面的算法,我们发现对于每个子串都有被重复检查。我们有必要重复检查每个字符后的所有子串么?答案是“没必要”。

我们可以使用和KMP字符串匹配算法相似的的策略:

提前计算下一个可能是子串开头的地方,而不是从头开始计算。

回顾题目我们在查找一个由词组L中所有单词组成的字符串并且单词的长度是固定的。为了覆盖所有的可能性,我们可以从字符串S的任意字符S[0,1,…,k-1]开始搜索所有的结果。也就是我们需要检查0,k,2k,…,n-1;然后1,k+1,2k+1,…,n-1;直到k-1,2k-1,3k-1,…,n-1。

从字符串S的第一个字符(begin = 0),截取一个长度为k的字符串子串,

  • 如果该子串不是词组L中的单词,搜索下一个子串(begin + = k);
  • 如果该子串是词组L中的单词但是已经被搜索过了(比如,重复的单词),向后移动begin直到重复的单词从当前的计算窗口移除了
  • 否则,将该子串加入当前集合。如果我们计算完所有的单词(例如,得到一个正确的结果),向后移动begin检查下一个字符串子串

现在这个算法的复杂度是什么?

我们检查每个字符串最多两次,一次将字符串加到集合中,另外一次将字符串从集合中移除。因此时间复杂度是O(n),这里n是字符串的长度。检查每个字符串是否是词组L中的单词,复杂度O(k)=O(1)。所以实际上的时间复杂度是O(n*k) = O(n)。

private void addWord(String w, HashMap<String, Integer> words) {  
if (words.containsKey(w)) {  
     		words.put(w, words.get(w)+1);  
   		} else {  
     		words.put(w, 1);  
   		}  
 	}  

 	private void removeWord(String w, HashMap<String, Integer> words) {  
   		if (!words.containsKey(w)) return;  
   		if (words.get(w) > 1) {  
     		words.put(w, words.get(w)-1);  
   		} else {  
     		words.remove(w);  
   		}  
 	}  

 	private int slideWindow(String S, int begin, int wordLen, HashMap<String, Integer> words) {  
   		String old = S.substring(begin, begin+wordLen);  
   		addWord(old, words);  
   		return begin+wordLen;  
 	}  

 	public ArrayList<Integer> findSubstring(String S, String[] L) {  
   		ArrayList<Integer> indices = new ArrayList<Integer>();  
   		if (L.length == 0) return indices;  

   		int total = L.length, wordLen = L[0].length();  

   		// store the words and frequencies in a hash table  
   		HashMap<String, Integer> expectWords = new HashMap<String, Integer>();  
   		for (String w : L) {  
     		addWord(w, expectWords);  
   		}  

   		// find concatenations  
   		for (int i=0; i < wordLen; ++i) {  
     		// check if there are any concatenations  
     		int count = 0;  
     		HashMap<String, Integer> collectWords = new HashMap<String, Integer>(expectWords);  
     		for (int j = i, begin = i; j <= S.length() - (total-count)*wordLen && begin <= S.length() - total*wordLen;) {  
      			String sub = S.substring(j, j+wordLen);  
       			if (!expectWords.containsKey(sub)) { // if not an expect word, reset  
         			begin = j + wordLen;  
         			j = begin;  
         			count = 0;  
         		collectWords.putAll(expectWords);  
       			} else if (!collectWords.containsKey(sub)) { // if duplicate, forward begin by 1  
         			begin = slideWindow(S, begin, wordLen, collectWords);  
       			} else {  
         			removeWord(sub, collectWords);  
         			j += wordLen;  
         			++count;  
         			if (collectWords.isEmpty()) {  
           				indices.add(begin);  
           				begin = slideWindow(S, begin, wordLen, collectWords);  
           				--count;  
         			}  
       			}  
     		}  
   		}  

   		return indices;  
 		}  	
 	

优化:原来的解法使用map的putAll浪费的大量的时间,以下是我优化后的,减少了大量的map复制时间(leetCode里面结果从59ms->17ms)

	public ArrayList<Integer> findSubstring(String s, String[] words) {
    ArrayList<Integer> indices = new ArrayList<>();
    int total, wordLen;
    if ((total = words.length) == 0 || (wordLen = words[0].length()) == 0 || s.length() < wordLen) return indices;

    // store the words and frequencies in a hash table
    HashMap<String, Integer> expectWords = new HashMap<>();
    for (String w : words) {
        addWord(w, expectWords);
    }

    HashMap<String, Integer> check = new HashMap<>();

    // find concatenations
    for (int i = 0; i < wordLen; i++) {
        // check if there are any concatenations
        check.clear();
        int begin = i, cursor = i;
        while (begin <= s.length() - total * wordLen) {
            String sub = s.substring(cursor, cursor + wordLen);
            if (!expectWords.containsKey(sub)) {
                begin = cursor + wordLen;
                cursor = begin;
                check.clear();
            } else if (Objects.equals(check.get(sub), expectWords.get(sub))) {
                // if duplicate, forward begin
                removeWord(s.substring(begin, begin + wordLen), check);
                begin += wordLen;
            } else {
                addWord(sub, check);
                cursor += wordLen;
                if (cursor - begin == total * wordLen) {
                    indices.add(begin);
                    begin += wordLen;
                    cursor = begin;
                    check.clear();
                }
            }
        }
    }

    return indices;
}