LeetCode热题100(Java)(4)子串

本章包括的题目有:

560. 和为 K 的子数组 - 力扣(LeetCode)

239. 滑动窗口最大值 - 力扣(LeetCode)

76. 最小覆盖子串 - 力扣(LeetCode)

1.和为K的子数组

思路解析:

要在数组中找到所有和为 k 的连续子数组,只需先计算出每个位置的前缀和,然后利用前缀和的差值来快速得到任意子数组的和。可以暴力枚举所有起点和终点的子数组,最终返回计数结果。

代码实现:

java 复制代码
class Solution {
    public int subarraySum(int[] nums, int k) {
        int ret = 0;
        int[] qzh = new int[nums.length];
        int sum = 0;
        for(int i = 0;i < nums.length;i++){
            sum += nums[i];
            qzh[i] = sum;
        }
        for (int i = 0; i < nums.length; i++) {
            if(qzh[i] == k) ret ++;
            for (int j = i + 1; j < nums.length; j++) {
                if(qzh[j] - qzh[i] == k) ret ++;
            }
        }
        return ret;
    }
}

这种方式简单暴力,但是性能很差

时间复杂度O(n*n)

空间复杂度O(n)

要找两个前缀和的差值等于K,实际上,我们知道目标值和一个前缀和,只需寻找数组中是否存在另一个前缀和。因此,我们可以用HashMap来快速寻找。(不用HashSet的原因是不同位置可能出现相同的前缀和)

java 复制代码
import java.util.HashMap;
import java.util.Map;

class Solution {
    public int subarraySum(int[] nums, int k) {
        // 哈希表:key = 前缀和,value = 该前缀和出现的次数
        Map<Integer, Integer> qzh = new HashMap<>();
        // 初始化:前缀和为0表示空子数组,出现1次
        qzh.put(0, 1);
        int sum = 0;   // 当前位置的前缀和
        int ret = 0;

        for (int num : nums) {
            sum += num;
            // 若之前出现过前缀和 sum - k,则从那些位置到当前位置的子数组和为k
            ret += qzh.getOrDefault(sum - k, 0);
            // 将当前前缀和存入,次数+1
            qzh.put(sum, qzh.getOrDefault(sum, 0) + 1);
        }

        return ret;
    }
}

时间复杂度O(n)

空间复杂度O(n)

2.滑动窗口最大值

思路解析:

要在一个数组中找出每个固定长度滑动窗口的最大值,只需维护当前窗口的最大值,并在窗口滑动时根据移出和移入的元素情况决定是否需要重新计算。用一个变量 max 记下当前窗口的最大值,窗口用左右指针 l 和 r 表示边界。

首先计算第一个窗口(0 到 k-1)的最大值存入 max。之后进入循环,每次将当前窗口的最大值 max 记录到结果数组中,然后窗口右移一位。此时分三种情况处理:

若新元素小于 max 且移出的不是 max,则最大值不变,直接处理下一轮。

若新元素大于或等于 max,则它直接成为新的最大值。

若移出的恰好是 max,说明当前最大值离开了窗口,需要重新扫描窗口 [l, r] 找出新的最大值。扫描时若遇到一个值等于旧的 max(说明还有相同最大值在窗口内),可以提前终止扫描,因为旧的 max 依然可以作为最大值。

反复推进窗口,直到右指针越界,最终返回记录的结果数组。

代码实现:

java 复制代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] arr = new int[k];
        int max = nums[0];
        int[] ans = new int[nums.length - k + 1];
        for(int i = 0;i < k;i ++){
            arr[i] = nums[i];
            if(max < nums[i])max = nums[i];
        }
        int l = 0,r = k - 1;
        while(r < nums.length){
            ans[l] = max;
            l++;r++;
            if(r == nums.length) break;
            if(nums[r] < max && nums[l - 1] != max)continue;
            if(nums[r] >= max) {
                max = nums[r];
                continue;
            }
            if(nums[l - 1] == max){
                int pre = max;
                max = nums[l];
                for(int i = l;i <= r; i ++){
                    if(max < nums[i])max = nums[i];
                    if(max == pre) break;
                }
            }
        }
        return ans;
    }
}

时间复杂度O(n*k)

空间复杂度O(k)

为消除重新扫描窗口的开销,可以用双端队列维护窗口内可能成为最大值的元素索引,队列内元素对应的值保持严格递减,这样每次最大值就是队头元素,且每个元素最多进出队列一次,实现线性时间复杂度。

实现思路:

要在数组中找出每个长度为 k 的滑动窗口的最大值,可以维护一个存储索引的双端队列,队列内索引对应的数组值保持递减。遍历数组时,对每个新元素执行以下步骤:

维护队列的递减性:将队列尾部所有对应值小于等于当前元素值的索引弹出,因为这些旧元素在当前元素存在的情况下不可能成为窗口内的最大值。

将当前索引加入队尾。

移除过期索引:检查队头索引是否已经滑出窗口(即队头索引 ≤ i - k),若是则弹出队头。

记录当前窗口最大值:当遍历到的索引 i ≥ k - 1 时,窗口形成,队头索引对应的元素即为当前窗口的最大值,存入结果数组。

这样,窗口每次滑动仅需常数时间维护队列,整个过程无需重新扫描。

代码实现:

java 复制代码
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[] ans = new int[n - k + 1];
        Deque<Integer> deque = new ArrayDeque<>(); // 存储索引,保证值递减
        
        for (int i = 0; i < n; i++) {
            // 移除队列尾部所有小于等于当前值的索引
            while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
                deque.pollLast();
            }
            // 加入当前索引
            deque.offerLast(i);
            // 移除头部已离开窗口的索引
            if (deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }
            // 记录窗口最大值(窗口形成后)
            if (i >= k - 1) {
                ans[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        return ans;
    }
}

时间复杂度O(n)

空间复杂度O(k)

3.最小覆盖子串

思路解析:

要在字符串 s 中找到覆盖 t 所有字符的最短子串,可以用滑动窗口。先统计 t 中各字符的需求量存入哈希表 need。用左右指针 left 和 right 维护窗口,右指针不断右移扩展窗口,将新字符加入另一个哈希表 window,若某字符的窗口内数量达到需求,则满足条件的字符种类数 valid 加一。当 valid 等于 need 的大小(即窗口已覆盖 t 的全部字符),就尝试右移左指针收缩窗口,每次收缩前用当前窗口长度更新最小子串的起始位置和长度。收缩时移出窗口左端的字符,若该字符原本刚好满足需求,减少后会导致 valid 减一,随后更新窗口计数。右指针遍历完整个 s 后,根据记录的最短长度截取子串返回

代码实现:

java 复制代码
import java.util.HashMap;
import java.util.Map;

class Solution {
    public String minWindow(String s, String t) {
        if (s.length() < t.length()) return "";

        // 记录 t 中每个字符需要的出现次数
        Map<Character, Integer> need = new HashMap<>();
        for (char c : t.toCharArray()) {
            need.put(c, need.getOrDefault(c, 0) + 1);
        }

        // 记录当前窗口中每个字符的出现次数
        Map<Character, Integer> window = new HashMap<>();
        
        int left = 0, right = 0;           
        int valid = 0;                     
        int start = 0, minLen = Integer.MAX_VALUE; 

        while (right < s.length()) {
            // 扩大窗口:移入字符 c
            char c = s.charAt(right);
            right++;
            if (need.containsKey(c)) {
                window.put(c, window.getOrDefault(c, 0) + 1);
                if (window.get(c).equals(need.get(c))) {
                    valid++;
                }
            }

            // 尝试缩小窗口:当窗口已经包含 t 的所有字符时
            while (valid == need.size()) {
                // 更新最小子串
                if (right - left < minLen) {
                    start = left;
                    minLen = right - left;
                }

                char d = s.charAt(left);
                left++;
                if (need.containsKey(d)) {
                    if (window.get(d).equals(need.get(d))) {
                        valid--;
                    }
                    window.put(d, window.get(d) - 1);
                }
            }
        }

        return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
    }
}

时间复杂度O(n)

空间复杂度O(1)

上一章:

LeetCode热题100(Java)(3)滑动窗口-CSDN博客

下一章:

未完待续...

相关推荐
Seven972 小时前
Tomcat Service的设计和实现:StandardService
java
python开发笔记2 小时前
Java(4) maven 结合spring 3 种框架设计架构
java·spring·maven
一只数据集2 小时前
机器学习多领域综合数据集分析-包含基因表达时间序列分类回归数据-适用于算法训练模型评估科研应用
人工智能·算法·数据分析
MY_TEUCK2 小时前
【Maven基础】Maven从安装配置到依赖管理与生命周期(可复现+避坑+面试)
java·面试·maven
huipeng9262 小时前
分布式服务部署详解
java·开发语言·spring cloud·微服务
秦歌6662 小时前
RAG-6-高级RAG实战案例:自适应路由 + 自评估重写 + 网络回退
java·服务器·前端·人工智能·python
c++之路2 小时前
C++ 命名空间(Namespace)
开发语言·c++·算法
jiang_bluetooth3 小时前
奈奎斯特第一准则理解和WIFI OFDM的关联
算法
极客先躯3 小时前
高级java每日一道面试题-2025年11月17日-容器与虚拟化题[Dockerj]-请解释容器和虚拟机的本质区别,从架构层面详细说明。
java·docker·架构