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

43 篇文章 0 订阅
28 篇文章 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 遍。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值