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

数据结构算法学习:LeetCode热题100-子串篇(和为 K 的子数组、滑动窗口最大值、最小覆盖子串)

文章目录

  • 简介
  • 560. 和为 K 的子数组
    • 问题描述
    • 解题方法
      • 暴力求解
      • 前缀和加哈希表
  • 239. 滑动窗口最大值
    • 问题描述
    • 解题方法
      • 单调队列加滑动窗口求解
  • 76. 最小覆盖子串
    • 问题描述
    • 解题方法
      • 哈希表+滑动窗口
  • 个人学习总结

简介

本博客聚焦于子串问题中的经典算法应用,重点探讨了滑动窗口技术和**队列(栈)**的使用。通过三个典型题目——560.和为K的子数组、239.滑动窗口最大值、76.最小覆盖子串,详细展示了从暴力解法到优化解法的演进过程。每个题目均包含问题描述、多种解题思路(如前缀和+哈希表、单调队列、滑动窗口+哈希表)、代码实现及复杂度分析。特别地,在76题中深入剖析了Java中Integer对象比较的陷阱,为读者提供了宝贵的实战经验。本博客旨在帮助读者深入理解子串问题的核心算法思想,掌握高效解决此类问题的技巧。

560. 和为 K 的子数组

问题描述

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。

示例

示例 1:
输入:nums = [1,1,1], k = 2
输出:2示例 2:
输入:nums = [1,2,3], k = 3
输出:2

标签提示: 数组、哈希表、前缀和

解题方法

暴力求解

解题思路
暴力求解的思路非常直接:枚举所有可能的连续子数组,计算每个子数组的和,统计其中和等于 k 的子数组数量。
解题步骤

  1. 初始化计数器 count = 0
  2. 使用双重循环遍历所有可能的子数组:
    • 外层循环 i 从 0 到 n-1,表示子数组的起始位置
    • 内层循环 j 从 i 到 n-1,表示子数组的结束位置
  3. 在内层循环中:
    • 维护一个变量 sum,表示从 i 到 j 的子数组和
    • 每次内层循环迭代时,sum 加上 nums[j]
    • 如果 sum == k,则计数器 count 加 1
  4. 循环结束后返回 count

代码实现

class Solution {public int subarraySum(int[] nums, int k) {int count = 0, n = nums.length;for(int i = 0; i < n; i ++){int sum = 0;for(int j = i; j < n; j ++){sum += nums[j];if(sum == k){count ++;}}}return count;}
}

复杂度分析
时间复杂度:O(n²),因为有两层嵌套循环
空间复杂度:O(1),只使用了常数个额外变量

前缀和加哈希表

解题思路
前缀和是一种高效计算子数组和的技术。定义前缀和 pre_num 为数组从开头到当前位置的和。核心思想是:

  • 子数组 nums[i…j] 的和 = pre_num[j] - pre_num[i-1]
  • 要使子数组和等于 k,需要满足:pre_num[j] - pre_num[i-1] = k
  • 转化为:pre_num[i-1] = pre_num[j] - k

使用哈希表存储前缀和出现的次数,可以在遍历时快速查找满足条件的前缀和。

解题步骤

  1. 初始化:
    • 计数器 count = 0
    • 当前前缀和 pre_num = 0
    • 哈希表 map,初始存入 {0: 1}(表示前缀和为0出现1次)
  2. 遍历数组:
    • 更新当前前缀和:pre_num += nums[i]
    • 检查哈希表中是否存在 pre_num - k:
      • 如果存在,说明有子数组和为 k,将对应的出现次数加到 count
    • 更新哈希表:
      • 将当前前缀和 pre_num 的出现次数加1
  3. 遍历结束后返回 count

代码实现:

class Solution {public int subarraySum(int[] nums, int k) {int count = 0, n = nums.length;int pre_num = 0;Map<Integer, Integer> map = new HashMap<>();map.put(0,1); // 初始化:前缀和为0出现1次for(int i = 0; i < n; i ++){pre_num += nums[i]; // 更新当前前缀和// 检查是否存在 pre_num - k 的前缀和if(map.containsKey(pre_num - k)){count += map.get(pre_num - k);} // 更新哈希表:当前前缀和出现次数+1map.put(pre_num, map.getOrDefault(pre_num, 0) + 1);}return count;}
}

复杂度分析
时间复杂度:O(n),只遍历数组一次,哈希表操作为O(1)
空间复杂度:O(n),哈希表最多存储n个不同的前缀和

239. 滑动窗口最大值

问题描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。

示例

示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       31 [3  -1  -3] 5  3  6  7       31  3 [-1  -3  5] 3  6  7       51  3  -1 [-3  5  3] 6  7       51  3  -1  -3 [5  3  6] 7       61  3  -1  -3  5 [3  6  7]      7示例 2:
输入:nums = [1], k = 1
输出:[1]

标签提示: 队列、数组、滑动窗口

解题方法

单调队列加滑动窗口求解

解题思想
使用单调递减双端队列来维护窗口内的元素,队列中存储的是数组元素的索引(而非值),并保证队列中索引对应的值始终单调递减。这样队首元素始终是当前窗口的最大值。核心思想:

  1. 维护单调性:队列中存储的索引对应的值从队首到队尾单调递减
  2. 动态更新:当窗口滑动时,移除不在窗口内的元素,并加入新元素时保持单调性
  3. 高效获取最大值:队首元素始终是当前窗口的最大值

解题步骤

  1. 初始化:
    • 创建双端队列 deque 存储索引
    • 创建结果数组 result,长度为 n - k + 1
  2. 遍历数组(索引 i 从 0 到 n-1):
    • a. 维护单调性(从队尾移除):
      • 当队列非空且队尾元素对应的值 ≤ 当前元素 nums[i] 时,移除队尾元素
      • 重复此过程直到队列为空或队尾元素 > 当前元素
    • b. 加入新元素:
      • 将当前索引 i 加入队尾
    • c. 移除过期元素(从队首移除):
      • 当队首元素索引 ≤ i - k(即不在当前窗口内)时,移除队首元素
    • d. 记录最大值:
      • 当 i ≥ k - 1(窗口已形成)时,将队首元素对应的值存入结果数组
  3. 返回结果:
    • 遍历完成后返回结果数组

代码实现:

import java.util.Deque;
import java.util.LinkedList;class Solution {public int[] maxSlidingWindow(int[] nums, int k) {if (nums == null || nums.length == 0 || k <= 0) {return new int[0];}int n = nums.length;int[] result = new int[n - k + 1];Deque<Integer> deque = new LinkedList<>(); // 存储索引for (int i = 0; i < n; i++) {// 1. 维护单调性:移除队尾所有小于等于当前元素的索引while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {deque.pollLast();}// 2. 加入新元素索引deque.offerLast(i);// 3. 移除过期元素(不在窗口内的队首元素)if (deque.peekFirst() <= i - k) {deque.pollFirst();}// 4. 记录当前窗口最大值if (i >= k - 1) {result[i - k + 1] = nums[deque.peekFirst()];}}return result;}
}

复杂度分析
时间复杂度:O(n)

  • 元素遍历:每个元素被处理一次(入队操作)
  • 元素出队:每个元素最多出队一次(从队尾或队首)
    • 队尾出队:保持单调性(每个元素最多被移除一次)
    • 队首出队:移除过期元素(每个元素最多被移除一次)
  • 总操作次数:每个元素最多执行2次操作(入队+出队)
  • 结果记录:每次窗口滑动记录一次结果(O(1)时间)

关键点:虽然代码中有嵌套循环(while循环),但每个元素最多被处理两次,因此总时间复杂度为线性O(n),而非O(nk)。

空间复杂度:O(k)

  • 双端队列:
    • 队列中最多存储k个元素(窗口大小)
    • 最坏情况:数组严格递减时,队列存储整个窗口元素
  • 结果数组:
    • 大小为n-k+1(题目要求输出)
    • 通常不计入额外空间复杂度(视为必要输出)
  • 额外空间:
    • 仅双端队列使用O(k)额外空间
    • 其他变量(索引、临时变量)使用O(1)空间

76. 最小覆盖子串

问题描述

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例

示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

标签提示: 哈希表、字符串、滑动窗口

解题方法

哈希表+滑动窗口

解题思想
使用滑动窗口(双指针)技术,在字符串s中寻找包含t中所有字符的最小子串。具体思路:

  1. 统计t中每个字符的频率(使用哈希表mapt)。
  2. 使用两个指针(begin和i)表示窗口的左右边界,初始化为0。
  3. 移动右指针i扩展窗口,直到窗口包含t中所有字符(通过计数count判断)。
  4. 当窗口满足条件时,尝试移动左指针begin缩小窗口,以找到更小的满足条件的子串。
  5. 在缩小窗口过程中,更新最小子串的起始位置和长度。
  6. 最终返回最小子串,若不存在则返回空字符串。
在这里插入代码片

解题步骤:

  1. 初始化:

    • 如果s的长度小于t,直接返回空字符串。
    • 创建两个哈希表:mapt记录t中每个字符的频率,mapwin记录当前窗口中每个字符的频率。
    • 遍历t,初始化mapt。
  2. 滑动窗口遍历:

    • 使用右指针i遍历s,对于每个字符c:
      • 如果c在t中,则更新mapwin中c的计数。
      • 如果mapwin中c的计数等于mapt中c的计数,则将count加1(表示一个字符已满足要求)。
    • 当count等于mapt的大小(即t中所有字符都满足要求)时,进入内层循环:
      • 更新最小子串:如果当前窗口长度(i-begin+1)小于之前记录的最小长度,则更新最小长度和起始、结束位置。
      • 移动左指针begin缩小窗口:
        • 如果左指针指向的字符a在t中,则更新mapwin中a的计数。
        • 如果mapwin中a的计数在减少前等于mapt中a的计数,则将count减1(表示该字符不再满足要求)。
        • 将a的计数减1,并移动左指针begin。
  3. 返回结果:

    • 如果没有找到满足条件的子串(ansb仍为-1),返回空字符串。
    • 否则,返回从ansb到anse的子串。

解题代码:

class Solution {public String minWindow(String s, String t) {int ls = s.length(), lt = t.length();if(ls < lt){return "";}Map<Character, Integer> mapt = new HashMap<>();Map<Character, Integer> mapwin = new HashMap<>();for(char strt : t.toCharArray()){mapt.put(strt, mapt.getOrDefault(strt, 0) + 1);}int count = 0, begin = 0, minlen = Integer.MAX_VALUE, ansb = -1, anse = -1;for(int i = 0; i < ls; i ++){// 添加字符char c = s.charAt(i);if(mapt.containsKey(c)){mapwin.put(c, mapwin.getOrDefault(c, 0) + 1);if(mapwin.get(c).equals(mapt.get(c))){count ++;}}while(count == mapt.size()){// 更新最短结果if(i - begin + 1 < minlen){minlen = i - begin + 1;ansb = begin;anse = i;}// 缩小窗口char a = s.charAt(begin);if(mapt.containsKey(a)){if(mapwin.get(a).equals(mapt.get(a))){count --;}mapwin.put(a, mapwin.get(a) - 1);}begin ++;}}if(ansb == -1){return "";}return s.substring(ansb, anse + 1);}
}

注意:Integer比较陷阱
在Java中,Integer对象在-128到127之间会被缓存,超出这个范围会创建新对象。因此,使用==比较两个Integer对象时,如果值在缓存范围内,比较的是值(因为指向同一个对象);如果超出缓存范围,比较的是对象引用(即使值相同,也可能返回false)。我在这里卡了很久,没想到是这个基础问题导致的。

复杂度分析
时间复杂度:O(|s| + |t|),其中|s|和|t|分别是字符串s和t的长度。

  • 初始化mapt需要遍历t,时间复杂度O(|t|)。
  • 遍历s时,每个元素最多被访问两次(一次被右指针i,一次被左指针begin),因此时间复杂度为O(|s|)。

空间复杂度:O(|Σ|),其中Σ是字符集的大小(例如ASCII字符集为128或256)。

  • 使用了两个哈希表,存储字符频率,最坏情况下需要存储所有不同的字符。

总结
本题通过滑动窗口和哈希表高效解决了最小覆盖子串问题。在实现时,需特别注意Java中Integer对象的比较陷阱,避免因引用比较导致的逻辑错误。修复后,代码能够正确处理所有测试用例,包括极端情况(如长字符串导致字符计数超过127)。

个人学习总结

通过本次对子串问题的系统学习,我深刻体会到滑动窗口技术在处理连续子数组/子串问题中的强大威力。以下是我的主要收获与感悟:

  1. 滑动窗口技术的灵活应用:在76题最小覆盖子串中,通过双指针动态调整窗口边界,结合哈希表统计字符频率,实现了高效的最小子串查找。这让我认识到,滑动窗口不仅适用于固定大小窗口(如239题),也能动态调整窗口大小以满足特定条件。
  2. 前缀和与哈希表的结合:560题展示了前缀和与哈希表的巧妙结合,将子数组和问题转化为前缀和差值问题,将时间复杂度从O(n²)优化到O(n)。这启示我,在遇到求和类问题时,应优先考虑前缀和优化。
  3. 单调队列的威力:239题中,单调队列的引入使得滑动窗口最大值问题的时间复杂度从O(nk)优化到O(n)。通过维护一个单调递减队列,确保队首始终是当前窗口的最大值,这种思想在处理极值问题时非常高效。
  4. 细节决定成败:在76题的实现中,我因忽略了Java中Integer对象的缓存机制(-128到127),导致使用==比较时出现逻辑错误。这提醒我,在编程时必须注意基础数据类型的特性,尤其是对象比较时,应使用equals方法而非==,以避免因缓存机制导致的陷阱。
  5. 复杂度分析的重要性:通过对每个解法进行时间复杂度和空间复杂度的分析,我学会了如何评估算法的效率,并理解了优化解法(如前缀和、单调队列)为何能大幅提升性能。这为我后续设计高效算法提供了理论指导。

总之,子串问题看似简单,但蕴含着丰富的算法思想。通过深入理解滑动窗口、前缀和、单调队列等核心技术,并注重编程细节,我们能够高效解决此类问题。这次学习不仅提升了我的算法能力,也让我更加注重代码的健壮性和细节处理。

http://www.dtcms.com/a/407802.html

相关文章:

  • 投资网站模板太原做网站哪家好
  • 营销网站开发isuos常州seo外包
  • 网站的基础建设项目网站平台建设的作用
  • 【EE初阶 - 网络原理】Socket 套接字
  • 2025 9月25 最近两周的问题
  • golang做网站企业年金办法
  • 南京网站建设王道下拉??智能网站建设报价
  • 网站建设色系搭配企业简介介绍
  • 国内做网站的公司有哪些如何在局域网中做网站
  • wordpress仿站教程WordPress拍卖模板
  • app开发和网站开发的区别做同行的旅游网站
  • 做网站网站赚怎么买到精准客户的电话
  • 操作系统进程同步与互斥核心知识点复习
  • 网站推广方案中网站图片模板
  • 网站建设好处网络营销渠道
  • 网页模板免费资源整站优化包年
  • 网站图片动态换名一对一专属定制方案
  • 网站建设销售实习建筑网官网查证
  • Express路由设计最佳实践
  • 如何成为一名合格的Java架构师
  • 亚马逊seo是什么意思seo策略分析
  • 网站优化年报告seo整站优化费用
  • 【系统分析师】2025年上半年真题:综合知识-答案及详解(回忆版)
  • 0、计算机硬件 —— 主板
  • 做网站需要的流程东莞网站关键词优化收费
  • 基于 OpenCV Eigenfaces 的人脸识别实战与原理解析
  • 网站开发工程师职责wordpress post 插件
  • 预处理 讲解
  • Redis持久化:RDB和AOF
  • 盛泽做网站的怎么做自己下单的网站