当前位置: 首页 > news >正文

Leetcode 30. 串联所有单词的子串

Leetcode 30. 串联所有单词的子串 是一道经典的字符串滑动窗口问题。这题考察字符串切分、哈希表匹配以及滑动窗口的灵活应用。解题难度较高,需要对循环和判断逻辑有良好的掌握。


题目描述

给定一个字符串 s 和一组长度都相同的单词数组 words,找出 s 中恰好可以由数组中所有单词连接在一起的连续子字符串的起始索引。这些单词在子字符串中可以以任意顺序排列,但每个单词必须完全匹配。


示例

输入: s = "barfoothefoobarman", words = ["foo","bar"]
输出: [0,9]
解释: 子串是 "barfoo" 和 "foobar",它们的起始索引分别为 0 和 9。

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

输入: s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出: [6,9,12]

解法 1:滑动窗口法

思路

将问题转化为固定宽度为 totalLen = words.length * wordLen 的滑动窗口问题:

  1. 窗口大小:
    • 由于单词长度都一样,窗口的大小固定为 totalLen
    • 可以遍历 s 的每个可能起点,从中截取长度为 totalLen 的窗口。
  2. 匹配机制:
    • 用一个 HashMap 记录 words 中每个单词的频次 (wordFreq)。
    • 每个窗口逐个截取长度为 wordLen 的单词片段,统计其出现频次 (currentFreq)。
    • 如果当前窗口内所有单词的出现频次完全匹配 wordFreq,则当前窗口为有效窗口。
  3. 滑动优化:
    • 滑动窗口通常从左到右依次移动,通过减少重新计算提升效率。

代码模板

import java.util.*;

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> result = new ArrayList<>();
        if (s == null || s.length() == 0 || words == null || words.length == 0) {
            return result;
        }

        int wordLen = words[0].length(); // 单词长度
        int totalLen = wordLen * words.length; // 总窗口长度
        
        // 单词频次统计
        Map<String, Integer> wordFreq = new HashMap<>();
        for (String word : words) {
            wordFreq.put(word, wordFreq.getOrDefault(word, 0) + 1);
        }

        // 滑动窗口
        for (int i = 0; i < wordLen; i++) { // 启动多个滑动窗口
            int left = i, right = i, count = 0;
            Map<String, Integer> currentFreq = new HashMap<>();

            while (right + wordLen <= s.length()) {
                String word = s.substring(right, right + wordLen);
                right += wordLen;

                // 处理当前单词
                if (wordFreq.containsKey(word)) {
                    currentFreq.put(word, currentFreq.getOrDefault(word, 0) + 1);
                    count++;

                    // 如果某单词超出频次限制,缩小窗口
                    while (currentFreq.get(word) > wordFreq.get(word)) {
                        String leftWord = s.substring(left, left + wordLen);
                        currentFreq.put(leftWord, currentFreq.get(leftWord) - 1);
                        left += wordLen;
                        count--;
                    }

                    // 检查是否完全匹配
                    if (count == words.length) {
                        result.add(left);
                    }
                } else {
                    // 非法单词,重置窗口状态
                    currentFreq.clear();
                    count = 0;
                    left = right;
                }
            }
        }

        return result;
    }
}

复杂度分析

  • 时间复杂度:
    • 单词数为 n,单词长度为 wordLen,字符串长度为 m
    • 滑动窗口遍历最多 m / wordLen 次,每次窗口内的频率判断需要 O(n)。
    • 总时间复杂度为 O(m * n / wordLen)
  • 空间复杂度: O(n)
    • 需要存储 wordFreqcurrentFreq 两个 HashMap。

适用场景

  • 适用于输入字符串较大,但单词数量较少的情况。
  • 高效处理,能够正确识别多种模式。

解法 2:分块扫描

思路

  • 类似滑动窗口,但优化起点扫描的策略。由于单词长度固定,字符串可以按照 wordLen 等分。
  • 对每个可能的起点进行独立的分块滑动扫描,减少重复逻辑。

代码模板

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> result = new ArrayList<>();
        if (s == null || s.length() == 0 || words == null || words.length == 0) {
            return result;
        }

        int wordLen = words[0].length();
        int totalLen = wordLen * words.length;
        if (s.length() < totalLen) {
            return result;
        }

        Map<String, Integer> wordFreq = new HashMap<>();
        for (String word : words) {
            wordFreq.put(word, wordFreq.getOrDefault(word, 0) + 1);
        }

        for (int i = 0; i < wordLen; i++) {
            Map<String, Integer> currentFreq = new HashMap<>();
            int left = i, count = 0;

            for (int right = i; right + wordLen <= s.length(); right += wordLen) {
                String word = s.substring(right, right + wordLen);

                if (wordFreq.containsKey(word)) {
                    currentFreq.put(word, currentFreq.getOrDefault(word, 0) + 1);
                    count++;

                    while (currentFreq.get(word) > wordFreq.get(word)) {
                        String removedWord = s.substring(left, left + wordLen);
                        currentFreq.put(removedWord, currentFreq.get(removedWord) - 1);
                        count--;
                        left += wordLen;
                    }

                    if (count == words.length) {
                        result.add(left);
                    }
                } else {
                    currentFreq.clear();
                    count = 0;
                    left = right + wordLen;
                }
            }
        }
        return result;
    }
}

复杂度分析

  • 时间复杂度: O(m * n / wordLen)。
  • 空间复杂度: O(n) (存储单词字典)。

解法 3:暴力法

思路

  1. 枚举字符串的所有子串,判断是否等于 words 的任意排列。
  2. 将字符串切分为固定大小的单词并比较频次。

代码模板

class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> result = new ArrayList<>();
        if (s == null || s.length() == 0 || words == null || words.length == 0) {
            return result;
        }

        int wordLen = words[0].length();
        int totalLen = wordLen * words.length;

        Map<String, Integer> wordFreq = new HashMap<>();
        for (String word : words) {
            wordFreq.put(word, wordFreq.getOrDefault(word, 0) + 1);
        }

        for (int i = 0; i <= s.length() - totalLen; i++) {
            String sub = s.substring(i, i + totalLen);
            Map<String, Integer> currentFreq = new HashMap<>();
            boolean isValid = true;

            for (int j = 0; j < sub.length(); j += wordLen) {
                String word = sub.substring(j, j + wordLen);
                currentFreq.put(word, currentFreq.getOrDefault(word, 0) + 1);

                if (!wordFreq.containsKey(word) || currentFreq.get(word) > wordFreq.get(word)) {
                    isValid = false;
                    break;
                }
            }

            if (isValid) {
                result.add(i);
            }
        }

        return result;
    }
}

复杂度分析

  • 时间复杂度: O(m * n)
    • 对于每个起点,我们需要遍历每个单词,再比较频次。
  • 空间复杂度: O(n)

快速 AC 策略

  1. 首选滑动窗口法 (解法 1):
    • 时间效率高,代码清晰,非常适合面试中快速实现。
  2. 可以考虑分块扫描 (解法 2):
    • 针对特定输入大小,可以优化性能。
  3. 避免使用暴力法 (解法 3):
    • 尽管清晰直观,但性能差,不适合大规模输入。

在面试或实际开发中,熟练掌握滑动窗口模板,能够快速解决这类问题!

相关文章:

  • 小鹏汽车申请注册“P7 Ultra”商标 或为P7车型升级版铺路
  • [java基础知识] java的集合体系Collection(List,Set,Queue),Map
  • 基于python跨平台硬件诊断的工具
  • 刷题 | 牛客 - js入门15题(更ing)5/15知识点解答
  • ubuntu 启动不起来,光标闪烁 解决方法
  • 杰和科技工业整机AF208|防尘+静音+全天候运行
  • GPU/CUDA 发展编年史:从 3D 渲染到 AI 大模型时代
  • 谈谈 HTTPS 的工作原理,SSL / TLS 握手流程是什么?
  • RabbitMQ怎么实现延时支付?
  • C++:内联函数
  • Linux常用指令
  • VirtualBox虚拟机安装Mac OS启动后的系统设置
  • 指纹细节提取(Matlab实现)
  • Java 大视界 -- Java 大数据在智能教育考试评估与学情分析中的应用(112)
  • RV1126的OSD模块和SDL_TTF结合输出H264文件
  • Elasticsearch简单学习
  • 电子电路中,正负双电源供电的需求原因
  • excel 斜向拆分单元格
  • 第51天:Web开发-JavaEE应用SpringBoot栈身份验证JWT令牌Security鉴权安全绕过
  • Webpack、Vite区别知多少?
  • 摩天大楼天津117大厦复工背后:停工近十年,未知挑战和压力仍在
  • 200枚篆刻聚焦北京中轴线,“印记”申遗往事
  • “上博号”彩绘大飞机今日启航:万米高空传播中国古代文化
  • 广东省副省长刘红兵跨省调任湖南省委常委、宣传部长
  • 工行一季度净赚841亿元降3.99%,营收降3.22%
  • 丁俊晖连续7年止步世锦赛16强,中国军团到了接棒的时候