数据结构与算法之深入解析“串联所有单词的子串”的求解思路与算法示例

Posted Serendipity·y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析“串联所有单词的子串”的求解思路与算法示例相关的知识,希望对你有一定的参考价值。

一、题目要求

  • 给定一个字符串 s 和一些长度相同的单词 words,找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
  • 注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
  • 示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:
从索引 09 开始的子串分别是 "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 <= 104;
    • s 由小写英文字母组成;
    • 1 <= words.length <= 5000;
    • 1 <= words[i].length <= 30;
    • words[i] 由小写英文字母组成。

二、求解算法

① 解法一

  • 首先,最直接的思路,判断每个子串是否符合,符合就把下标保存起来,最后返回即可。如下所示,利用循环变量 i ,依次后移,判断每个子串是否符合即可:

  • 怎么判断子串是否符合?这也是本题的难点,由于子串包含的单词顺序并不需要固定,如果是两个单词 A,B,我们只需要判断子串是否是 AB 或者 BA 即可。如果是三个单词 A,B,C 也还好,只需要判断子串是否是 ABC,或者 ACB,BAC,BCA,CAB,CBA 就可以了。
  • 但如果更多单词呢?那该怎么处理呢?我们可以用两个 HashMap 来解决:
    • 首先,把所有的单词存到 HashMap 里,key 直接存单词,value 存单词出现的个数(因为给出的单词可能会有重复的,所以可能是 1 或 2 或者其它);
    • 然后扫描子串的单词,如果当前扫描的单词在之前的 HashMap 中,就把该单词存到新的 HashMap 中,并判断新的 HashMap 中该单词的 value 是不是大于之前的 HashMap 该单词的 value;如果大了,就代表该子串不是我们要找的,接着判断下一个子串就可以了;如果不大于,那么接着判断下一个单词的情况。
    • 子串扫描结束,如果子串的全部单词都符合,那么该子串就是我们找的其中一个。
  • 如下所示,把 words 存到一个 HashMap 中:

  • 然后遍历子串的每个单词:

  • 第一个单词在 HashMap1 中,然后把 foo 存到 HashMap2 中,并且比较此时 foo 的 value 和 HashMap1 中 foo 的 value,1 < 2,所以继续扫描:

  • 第二个单词也在 HashMap1 中,然后把 foo 存到 HashMap2 中,因为之前已经存过了,所以更新它的 value 为 2 ,然后继续比较此时 foo 的 value 和 HashMap1 中 foo 的 value,2 <= 2,所以继续扫描下一个单词:

  • 第三个单词也在 HashMap1 中,然后把 foo 存到 HashMap2 中,因为之前已经存过了,所以更新它的 value 为 3,然后继续比较此时 foo 的 value 和 HashMap1 中 foo 的 value,3 > 2,所以表明该字符串不符合,然后判断下个子串就好了。
  • 当然上边的情况都是单词在 HashMap1 中,如果不在的话就更好说了,不在就表明当前子串肯定不符合了,直接判断下个子串就好了。
  • Java 示例:
public List<Integer> findSubstring(String s, String[] words) 
    List<Integer> res = new ArrayList<Integer>();
    int wordNum = words.length;
    if (wordNum == 0) 
        return res;
    
    int wordLen = words[0].length();
    // HashMap1 存所有单词
    HashMap<String, Integer> allWords = new HashMap<String, Integer>();
    for (String w : words) 
        int value = allWords.getOrDefault(w, 0);
        allWords.put(w, value + 1);
    
    // 遍历所有子串
    for (int i = 0; i < s.length() - wordNum * wordLen + 1; i++) 
        // HashMap2 存当前扫描的字符串含有的单词
        HashMap<String, Integer> hasWords = new HashMap<String, Integer>();
        int num = 0;
        // 判断该子串是否符合
        while (num < wordNum) 
            String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen);
            // 判断该单词在 HashMap1 中
            if (allWords.containsKey(word)) 
                int value = hasWords.getOrDefault(word, 0);
                hasWords.put(word, value + 1);
                // 判断当前单词的 value 和 HashMap1 中该单词的 value
                if (hasWords.get(word) > allWords.get(word)) 
                    break;
                
             else 
                break;
            
            num++;
        
        // 判断是不是所有的单词都符合条件
        if (num == wordNum) 
            res.add(i);
        
    
    return res;

  • C++ 示例:
class Solution 
public:
    vector<int> findSubstring(string s, vector<string>& words) 
        vector<int> ret;
        if(words.size() == 0) // 判断words为空,因为下面用到了words[0]
            return ret;
        
        int word_size = words[0].size();
        int word_num = words.size();
        
        unordered_map<string,int> m1;// 构造hashmap
        for(int i=0;i<word_num;i++)
            m1[words[i]]++;
        
        unordered_map<string,int> m2;
        for(int i=0; (i + word_size * word_num) <= s.size() ; i++)// 截取的s符合题意
            int j = 0;
            for(j=i;j < (i + word_size * word_num) ; j=j+word_size)// 分段判断
                string temp_str = s.substr(j,word_size);
                if(m1[temp_str] == 0) // m1中没有截取的串,直接跳出
                    break;
                else
                    m2[temp_str]++;
                    if(m1[temp_str] < m2[temp_str])// 重复次数过多,也跳出
                        break;
                
            
            
            if(j == (i + word_size * word_num)) // 每一段都符合,则加入答案
                ret.push_back(i);
            m2.clear(); // 清空m2
        
        return ret;
    
;

② 解法二

  • 在解法一中,每次移动一个字符:

  • 现在为了方便讨论,我们每次移动一个单词的长度,也就是 3 个字符,这样所有的移动被分成了三类:



  • 以上三类以第一类从 0 开始移动为例,讲一下如何对算法进行优化,有三种需要优化的情况:
    • 情况一:当子串完全匹配,移动到下一个子串的时候:在解法一中,对于 i = 3 的子串,我们肯定是从第一个 foo 开始判断。但其实前两个 foo 都不用判断了 ,因为在判断上一个 i = 0 的子串的时候已经判断过了,所以解法一中的 HashMap2 每次并不需要清空从 0 开始,而是可以只移除之前 i = 0 子串的第一个单词 bar 即可,然后直接从箭头所指的 foo 开始就可以了。

    • 情况二:当判断过程中,出现不符合的单词:但判断 i = 0 的子串的时候,出现了 the ,并不在所给的单词中,所以此时 i = 3,i = 6 的子串,其实并不需要判断,直接判断 i = 9 的情况就可以了。

    • 情况三:判断过程中,出现的是符合的单词,但是次数超了:
      • 对于 i = 0 的子串,此时判断的 bar 其实是在 words 中的,但是之前已经出现了一次 bar,所以 i = 0 的子串是不符合要求的。此时只需要往后移动窗口,i = 3 的子串将 foo 移除,此时子串中一定还是有两个 bar,所以该子串也一定不符合。接着往后移动,当之前的 bar 被移除后,此时 i = 6 的子串,就可以接着按正常的方法判断了。
      • 对于出现 i = 0 的子串的情况,可以直接从 HashMap2 中依次移除单词,当移除了之前次数超的单词的时候,就可以正常判断了,直接从移除了超出了次数的单词后,也就是 i = 6 开始判断就可以了。

  • Java 示例:
public List<Integer> findSubstring(String s, String[] words) 
    List<Integer> res = new ArrayList<Integer>();
    int wordNum = words.length;
    if (wordNum == 0) 
        return res;
    
    int wordLen = words[0].length();
    HashMap<String, Integer> allWords = new HashMap<String, Integer>();
    for (String w : words) 
        int value = allWords.getOrDefault(w, 0);
        allWords.put(w, value + 1);
    
    // 将所有移动分成 wordLen 类情况
    for (int j = 0; j < wordLen; j++) 
        HashMap<String, Integer> hasWords = new HashMap<String, Integer>();
        int num = 0; // 记录当前 HashMap2(这里的 hasWords 变量)中有多少个单词
		// 每次移动一个单词长度
        for (int i = j; i < s.length() - wordNum * wordLen + 1; i = i + wordLen) 
            boolean hasRemoved = false; // 防止情况三移除后,情况一继续移除
            while (num < wordNum) 
                String word = s.substring(i + num * wordLen, i + (num + 1) * wordLen);
                if (allWords.containsKey(word)) 
                    int value = hasWords.getOrDefault(word, 0);
                    hasWords.put(word, value + 1);
                    // 出现情况三,遇到了符合的单词,但是次数超了
                    if (hasWords.get(word) > allWords.get(word)) 
                        // hasWords.put(word, value);
                        hasRemoved = true;
                        int removeNum = 0;
                        // 一直移除单词,直到次数符合了
                        while (hasWords.get(word) > allWords.get(word)) 
                            String firstWord = s.substring(i + removeNum * wordLen, i + (removeNum + 1) * wordLen);
                            int v = hasWords.get(firstWord);
                            hasWords.put(firstWord, v - 1);
                            removeNum++;
                        
                        num = num - removeNum + 1; // 加 1 是因为我们把当前单词加入到了 HashMap 2 中
                        i = i + (removeNum - 1) * wordLen; // 这里依旧是考虑到了最外层的 for 循环,看情况二的解释
                        break;
                    
                // 出现情况二,遇到了不匹配的单词,直接将 i 移动到该单词的后边(但其实这里
                // 只是移动到了出现问题单词的地方,因为最外层有 for 循环, i 还会移动一个单词
                // 然后刚好就移动到了单词后边)
                 else 
                    hasWords.clear();
                    i = i + num * wordLen;
                    num = 0;
                    break;
                
                num++;
            
            if (num == wordNum) 
                res.add(i);
            
            // 出现情况一,子串完全匹配,将上一个子串的第一个单词从 HashMap2 中移除
            if (num > 0 && !hasRemoved) 
                String firstWord = s.substring(i, i + wordLen);
                int v = hasWords.get(firstWord);
                hasWords.put(firstWord, v - 1);
                num = num - 1;
            
        
    
    return res;

以上是关于数据结构与算法之深入解析“串联所有单词的子串”的求解思路与算法示例的主要内容,如果未能解决你的问题,请参考以下文章

☆打卡算法☆LeetCode 30串联所有单词的子串 算法解析

☆打卡算法☆LeetCode 30串联所有单词的子串 算法解析

Python描述 LeetCode 30. 串联所有单词的子串

算法leetcode|30. 串联所有单词的子串(rust重拳出击)

算法leetcode|30. 串联所有单词的子串(rust重拳出击)

算法leetcode|30. 串联所有单词的子串(rust重拳出击)