美文网首页
leetcode 30. 串联所有单词的子串(Java版)

leetcode 30. 串联所有单词的子串(Java版)

作者: M_lear | 来源:发表于2019-04-29 15:37 被阅读0次

    题目描述(题目难度,困难)

    给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。

    注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。

    示例 1:

    输入:
    s = "barfoothefoobarman",
    words = ["foo","bar"]
    输出:[0,9]
    解释:
    从索引 0 和 9 开始的子串分别是 "barfoor" 和 "foobar" 。
    输出的顺序不重要, [9,0] 也是有效答案。

    示例 2:

    输入:
    s = "wordgoodgoodgoodbestword",
    words = ["word","good","best","word"]
    输出:[]

    题目求解

    解法一,暴力法

    把 words 数组中的所有单词放入 HashMap ,同时统计每个单词的出现次数。借助 HashMap 快速查找的特点,来匹配 s 字符串。整体的思路很简单明了,就是挨个匹配。

    class Solution {
        public List<Integer> findSubstring(String s, String[] words) {
            List<Integer> result = new ArrayList<>();
            if(s.length() == 0 || words.length == 0) return result;
            Map<String, Integer> countMap = new HashMap<>();
            for(int i = 0; i < words.length; ++i){
                countMap.put(words[i], countMap.getOrDefault(words[i], 0)+1);
            }
            int wordLen = words[0].length();
            int wordsLen = words.length*wordLen;
            for(int i = 0; i <= s.length()-wordsLen; ++i){
                Map<String, Integer> workMap = new HashMap<>();
                int j;
                for(j = 0; j < words.length; ++j){
                    String word = s.substring(i+j*wordLen, i+(j+1)*wordLen);
                    workMap.put(word, workMap.getOrDefault(word, 0)+1);
                    if(workMap.get(word) > countMap.getOrDefault(word, 0)) break; // 匹配失败
                }
                if(j == words.length) result.add(i);
            }
            return result;
        }
    }
    

    注意 String 的 substring 方法的范围参数是 substring(int beginIndex, int endIndex) 区间前闭后开,除此之外还有 public CharSequence subSequence(int beginIndex, int endIndex)public int codePointCount(int beginIndex, int endIndex),其余方法都用 起始位置和长度 表示。

    暴力法每个 substring 会被取若干遍,最多取 words.length 遍。

    解法二,滑动窗口

    使用在 s 上的滑动窗口来匹配 words 数组,双指针 left,right 分别表示窗口的开始和结束。
    指针移动的单位长度为 words 数组中单词的长度 wordLen。
    每次取 s 上 right 到 right+wordLen 的一个新单词加入窗口。
    如果这个单词不在 words 数组中(借助 HashMap 判断),说明这个窗口不合格,就将 right 置为 right+wordLen,left 置为 right,继续找。
    如果这个单词在 words 数组中,但在窗口中的出现次数大于在 words 数组中的出现次数,说明这个窗口还是不合格,需要不断的将 left 加上 wordLen,即不断的删除窗口最左边的单词,直到这个单词在窗口中的出现次数与在 words 数组中的出现次数相等为止。
    否则就表示当前窗口的所有单词都在 words 数组中,窗口没有任何问题,当窗口长度 right-left 正好为 words 数组中所有单词的长度和时,就表示匹配成功,将 left 加入结果集。

    class Solution {
        public List<Integer> findSubstring(String s, String[] words) {
            List<Integer> result = new ArrayList<>();
            if(s.length() == 0 || words.length == 0) return result;
            Map<String, Integer> countMap = new HashMap<>();
            int wordLen = words[0].length();
            int wordsLen = words.length*wordLen;
            for(String word : words){
                countMap.put(word, countMap.getOrDefault(word, 0)+1);
            }
            for(int i = 0; i < wordLen; ++i){ // 错位遍历,保证所有情况都遍历到
                int left = i, right = i;
                Map<String, Integer> workMap = new HashMap<>();
                while(right+wordLen <= s.length()){
                    String rw = s.substring(right, right+wordLen);
                    right += wordLen;
                    if(!countMap.containsKey(rw)){ // 重置窗口
                        left = right;
                        workMap.clear();
                    }else{
                        workMap.put(rw, workMap.getOrDefault(rw, 0)+1);
                        while(workMap.get(rw) > countMap.get(rw)){ // 删除左边单词,使窗口符合要求
                            String lw = s.substring(left, left+wordLen);
                            workMap.put(lw, workMap.get(lw)-1);
                            left += wordLen;
                        }
                        if(right-left == wordsLen) result.add(left);
                    }
                }
            }
            return result;
        }
    }
    

    滑动窗口右指针右移取一遍 substring,左指针右移可能又取一遍,所以每个 substring 最多取两遍,但至少取一遍。

    解法三,更优的解法

    class Solution {
        public List<Integer> findSubstring(String s, String[] words) {
           List<Integer> list = new ArrayList<>();
            if (words.length == 0 || s.length() < words.length * words[0].length()) {
                return list;
            }
            Map<String, Integer> map = new HashMap<>();
            for (int i = 0; i < words.length; i++) {
                map.put(words[i], map.getOrDefault(words[i], 0) + 1);
            }
            int listLen = words.length;
            int wordLen = words[0].length();
    
            // 从 0 到 wordLen-1 依次遍历,保证能遍历所有情况。
            for (int i = 0; i < wordLen; i++) {
                for (int j = i; j <= s.length() - wordLen * listLen; j += wordLen) {
                    Map<String, Integer> map2 = new HashMap<>();
                    // 从尾往头遍历,判断往后第 k 个 len 位置的子串是否在 map 中
                    for (int k = listLen - 1; k >= 0; k--) {
                        String temp = s.substring(j + k * wordLen, j + (k + 1) * wordLen);
                        int val = map2.getOrDefault(temp, 0) + 1;
                        // 如果从 j+k*wordLen, j 到 (k+1)*wordLen 位置的子串不在 map 中
                        // 代表可以从 j 到 j+(k+1)*wordLen 这一段都可以舍弃
                        // 这里只需把 j 移动 k*wordLen,剩余的一个 wordLen 在循环体中移动。
                        if (val > map.getOrDefault(temp, 0)) {
                            j += k * wordLen;
                            break;
                        }
                        // k 到 0 代表找到了符合的子串
                        if (k == 0) {
                            list.add(j);
                        } else {
                            map2.put(temp, val);
                        }
                    }
                }
            }
            return list;
        }
    }
    

    匹配时从后往前遍历,同样最多取两遍 substring,但最少是 0 遍。

    相关文章

      网友评论

          本文标题:leetcode 30. 串联所有单词的子串(Java版)

          本文链接:https://www.haomeiwen.com/subject/fvaenqtx.html