算法-滑动窗口-串联所有单词的子串

文章介绍了如何使用滑动窗口和哈希表的策略解决LeetCode上的一个问题,即找到一个字符串s中的子串,这些子串连接起来恰好包含给定数组words中的所有单词。作者提供了两种方法,一种是一次性滑动窗口,另一种是双层滑动窗口,分析了时间复杂度和空间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

算法-滑动窗口-串联所有单词的子串

1 题目概述

1.1 题目出处

https://leetcode.cn/problems/substring-with-concatenation-of-all-words/

1.2 题目描述

在这里插入图片描述
在这里插入图片描述

2 滑动窗口+Hash表

2.1 解题思路

  1. 构建一个大小为串联子串的总长的滑动窗口
  2. 为每个words中的子串创建一个hash表, <子串值,子串出现次数>
  3. 记录每次开始位置,从左往右遍历字符串s,每次截取words[0]长度的子串,和2中hash表对比,如果没有或者使用次数超限,就表示该组合无法构成,退出该次检测;否则继续检测,直到构成的串联子串长度满足要求或者已检测长度超限。构建成功时,记录下起始序号
  4. 一轮检测完成后,窗口向右滑动1个长度,继续下一轮检测

2.2 代码

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> resultList = new ArrayList<>();
        int windowSize = words[0].length();
        if (windowSize > s.length()) {
            return resultList;
        }
        int strLength = windowSize * words.length;
        if (strLength > s.length()){
            return resultList;
        }
        Map<String, Integer> wordMap = new HashMap<>();
        for (int i = 0; i < words.length; i++) {
            Integer subKeyTimes = wordMap.get(words[i]);
            if (null == subKeyTimes) {
                subKeyTimes = 0;
            } 
            wordMap.put(words[i], subKeyTimes + 1);
        }

        for (int i = 0; i <= s.length() - strLength; i++) {
            int result = getStart(s, words, strLength, windowSize, i, wordMap);
            if (result != -1) {
                resultList.add(result);
            }
        }
        return resultList;
    }

    private int getStart(String s, String[] words, int strLength, int windowSize, int first, Map<String, Integer> wordMap) {
        int start = -1;
        int length = 0;
        Map<String, Integer> useMap = new HashMap();
        for (int i = first; i < s.length() && length < strLength && i < first + strLength; i += windowSize) {
            String sub = s.substring(i, i + windowSize);
            Integer subKeyTimes = wordMap.get(sub);
            if (null == subKeyTimes) {
                return -1;
            }
            Integer useTimes = useMap.get(sub);
            if (null == useTimes) {
                useTimes = 1; 
            } else {
                useTimes += 1;
            }
            if (useTimes > subKeyTimes) {
                return -1;
            }
            useMap.put(sub, useTimes);
            length += windowSize;
            if (start == -1) {
                start = i;
            }
        }
        if (length == strLength) {
            return start;
        }
        return -1;
    }
}

2.3 时间复杂度

在这里插入图片描述
s.length=N,words.length=M ,时间复杂度O(N*M)

2.4 空间复杂度

O(M)

3 双滑动窗口+Hash表

3.1 解题思路

使用两个滑动窗口:

  1. 外层滑动窗口,每次移动大小为1,最多移动words[0]长度
  2. 内层滑动窗口,每次移动大小为words[0]

相当于把words[0]长度倍数的子串放在一起,用内层滑动窗口进行解析:

  1. 如果遇到当前子串不在目标字符串s,则舍弃之前记录,从下一个子串开始
  2. 如果遇到当前子串在目标字符串s内:
    1. 使用次数未超限,则记录使用次数、首个拼接子串下标、并累加拼接长度,再看是否已经拼接目标串联子串成功:
      • 如果成功则记录下标并移除首个子串,继续检测下一个子串;
      • 如果未成功,直接继续继续检测下一个子串;
    2. 使用次数超限,则不断移除已拼接子串内首个子串,直到次数不超限为止,并继续检测下一个子串

3.2 代码

class Solution {
     public List<Integer> findSubstring(String s, String[] words) {
            List<Integer> resultList = new ArrayList<>();
            int windowSize = words[0].length();
            if (windowSize > s.length()) {
                return resultList;
            }
            int strLength = windowSize * words.length;
            if (strLength > s.length()){
                return resultList;
            }
            Map<String, Integer> wordMap = new HashMap<>();
            for (int i = 0; i < words.length; i++) {
                Integer subKeyTimes = wordMap.get(words[i]);
                if (null == subKeyTimes) {
                    subKeyTimes = 0;
                }
                wordMap.put(words[i], subKeyTimes + 1);
            }

            for (int i = 0; i < windowSize; i += 1) {
                List<Integer> subResultList = getStart(s, strLength, windowSize, i, wordMap);
                if (subResultList.size() > 0) {
                    resultList.addAll(subResultList);
                }
            }
            return resultList;
        }

        private List<Integer> getStart(String s, int strLength, int windowSize, int first, Map<String, Integer> wordMap) {
            List<Integer> resultList = new ArrayList();
            int start = -1;
            int length = 0;
            Map<String, Integer> useMap = new HashMap();

            for (int i = first; i <= s.length() - windowSize; i += windowSize) {
                String sub = s.substring(i, i + windowSize);
                Integer subKeyTimes = wordMap.get(sub);
                if (null == subKeyTimes) {
                    // 子串找不到,清空useMap
                    useMap.clear();
                    start = -1;
                    length = 0;
                    continue;
                }
                length += windowSize;
                Integer useTimes = useMap.get(sub);
                if (null == useTimes) {
                    useTimes = 1;
                } else {
                    useTimes += 1;
                    useMap.put(sub, useTimes);
                    while (useTimes > subKeyTimes) {
                        // 子串使用次数超出存在的子串条数,删除第一个子串计数,直到不超出为止
                        String firstSub = s.substring(start, start + windowSize);
                        useMap.put(firstSub, useMap.get(firstSub) - 1);
                        useTimes = useMap.get(sub);
                        start = start + windowSize;
                        length -= windowSize;
                    }
                }
                useMap.put(sub, useTimes);
                if (start == -1) {
                    start = i;
                }
                if (length == strLength) {
                    // 成功凑成一个串联子串
                    // 记录开始下标
                    resultList.add(start);
                    // 移除首个子串
                    String firstSub = s.substring(start, start + windowSize);
                    useMap.put(firstSub, useMap.get(firstSub) - 1);
                    start = start + windowSize;
                    length -= windowSize;
                }
            }

            return resultList;
        }
}

3.3 时间复杂度

s.length=N,words.length=M ,时间复杂度O(N)
在这里插入图片描述

3.4 空间复杂度

O(M)

参考文档

### 使用滑动窗口算法解决串联所有单词子串 #### 解决方案概述 为了处理 LeetCode 第 30 题的要求,即在字符串 `s` 中寻找由字符串数组 `words` 组成的所有可能的连续子串的位置,采用滑动窗口方法是一种高效的选择[^1]。 #### 参数定义与初始化 设定了几个重要的参数来辅助解决问题: - 设 `m = words.length` 表示单词的数量; - 设 `len` 是 `words` 数组中单个单词的长度,则整个窗口宽度应设定为 `len * m`;这代表了当我们在字符串 `s` 上应用此窗口时所期望匹配到的一系列连续字符总长度[^2]。 #### 实现思路 通过内外两层嵌套的方式实现滑动窗口机制: 外层循环负责遍历起点位置的变化,每次前进步长设置为单一单词长度(`len`)。对于每一个新的起始点而言, 内层则利用固定大小等于全部目标短语拼接后的整体尺寸(`len*m`)作为扫描单位,在当前选定范围内逐一对比是否存在符合条件的结果集[^4]。 具体操作如下所示: ```python from collections import Counter, defaultdict def findSubstring(s: str, words: List[str]) -> List[int]: if not s or not words: return [] word_length = len(words[0]) total_words = len(words) window_size = word_length * total_words # 记录每个单词出现次数 word_count = Counter(words) result_indices = [] for i in range(word_length): start = i current_word_counts = defaultdict(int) matched_words = 0 for j in range(i, len(s) - word_length + 1, word_length): substring = s[j:j + word_length] if substring in word_count: current_word_counts[substring] += 1 while current_word_counts[substring] > word_count[substring]: temp_start_str = s[start:start + word_length] current_word_counts[temp_start_str] -= 1 if current_word_counts[temp_start_str] >= word_count[temp_start_str]: matched_words -= 1 start += word_length matched_words += 1 if matched_words == total_words: result_indices.append(start) else: start = j + word_length current_word_counts.clear() matched_words = 0 return result_indices ``` 该函数首先检查输入的有效性并计算必要的变量值。接着创建了一个外部for循环用于控制初始偏移量i (范围是从0至word_length),从而确保能够捕捉到任何潜在解法而不遗漏任何一个可能性。内部逻辑则是基于上述提到的方法论构建而成,旨在有效地定位满足条件的索引列表。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值