【思路详解+详细注释】小白都能看懂的力扣算法详解——子串

一 LC76.最小覆盖子串

题目要求:

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

注意:

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

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

思路分析:

       要解决这个问题,可以使用滑动窗口技术结合字符计数。基本思路是通过一个窗口在字符串 s 中滑动,尽量找到最小的窗口,使得窗口中的字符涵盖了字符串 t 中的所有字符。

        我们使用一个哈希表记录t中每个字符出现的频率,另一个哈希表记录当前窗口中出现的字符频率。定义两个指针表示滑动窗口的左右边界,移动右指针扩展窗口,直到窗口中包括了t中的所有字符,之后我们收缩窗口,移动左指针,找到最小的窗口。

完整代码示例:

class Solution {
    public String minWindow(String s, String t) {
           // 如果 s 的长度小于 t 的长度,则不存在符合条件的子串,直接返回空字符串
        if (s.length() < t.length()) return "";

        // 创建哈希表,用于记录字符串 t 中每个字符的频率
        Map<Character, Integer> tCount = new HashMap<>();
        for (char c : t.toCharArray()) {
            tCount.put(c, tCount.getOrDefault(c, 0) + 1);
        }

        // 创建哈希表,用于记录当前窗口中每个字符的频率
        Map<Character, Integer> windowCount = new HashMap<>();
        int required = tCount.size(); // t 中唯一字符的数量
        int formed = 0; // 当前窗口中满足 t 中字符频率的字符数量
        int left = 0, right = 0; // 窗口的左右边界
        int[] ans = {-1, 0, 0}; // 最小窗口的大小、起始和结束位置

        // 开始滑动窗口
        while (right < s.length()) {
            char c = s.charAt(right); // 当前字符
            windowCount.put(c, windowCount.getOrDefault(c, 0) + 1); // 更新当前窗口中字符的频率

            // 如果当前字符在 t 中,并且窗口中字符的频率符合 t 中的要求
            if (tCount.containsKey(c) && windowCount.get(c).intValue() == tCount.get(c).intValue()) {
                formed++; // 满足条件的字符数量增加
            }

            // 当当前窗口满足条件时,尝试收缩窗口
            while (left <= right && formed == required) {
                c = s.charAt(left); // 窗口左边界的字符

                // 更新最小窗口的信息
                if (right - left + 1 < ans[0] || ans[0] == -1) {
                    ans[0] = right - left + 1; // 更新最小窗口的大小
                    ans[1] = left; // 更新最小窗口的起始位置
                    ans[2] = right; // 更新最小窗口的结束位置
                }

                // 收缩窗口
                windowCount.put(c, windowCount.get(c) - 1); // 更新当前窗口中字符的频率
                // 如果窗口中字符的频率不再符合 t 中的要求
                if (tCount.containsKey(c) && windowCount.get(c).intValue() < tCount.get(c).intValue()) {
                    formed--; // 满足条件的字符数量减少
                }

                left++; // 窗口左边界右移
            }

            right++; // 窗口右边界右移
        }

        // 如果 ans[0] 仍为 -1,说明没有找到符合条件的子串,返回空字符串;否则返回最小窗口的子串
        return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
    }
}

二 LC239.滑动窗口最大值

题目要求:

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

返回 滑动窗口中的最大值 

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

思路分析:

       我们可以使用双端队列来完成,设置双端队列存储最大值(便于O(1)时间获取),队列降序排列,一轮遍历,先把窗口塞满,然后这时就可以返回最大值(队头)了,但返回前要做两件事,首先,要加入数组当前下标到队尾,并保证是单调的,其次,如果队头这是已经到了窗口左边,就移除队头。

        这道题目比较难,大家可以参考一下左程云老师的视频课程,讲得很清楚易懂。

完整代码示例:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums == null || nums.length < 2) return nums;
        // 双向队列 保存当前窗口最大值的数组位置 保证队列中数组位置的数值按从大到小排序
        LinkedList<Integer> queue = new LinkedList();
        // 结果数组
        int[] result = new int[nums.length-k+1];
        // 遍历nums数组
        for(int i = 0;i < nums.length;i++){
            // 保证从大到小 如果前面数小则需要依次弹出,直至满足要求
            while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
                queue.pollLast();
            }
            // 添加当前值对应的数组下标
            queue.addLast(i);
            // 判断当前队列中队首的值是否有效
            if(queue.peek() <= i-k){
                queue.poll();   
            } 
            // 当窗口长度为k时 保存当前窗口中最大值
            if(i+1 >= k){
                result[i+1-k] = nums[queue.peek()];
            }
        }
        return result;
    }
}

三 LC560.和为k的子数组

题目要求:

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 

子数组是数组中元素的连续非空序列。

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

思路分析:

       做了两道困难题,做一道中等题目休息一下吧。和为k的子数组也是一道比较经典的题目了。我们可以使用前缀和和哈希表实现。也就是利用前缀和来确定子数组的和是否等于 k,同时利用哈希表来记录前缀和的出现次数。具体做法为:在遍历数组的过程中,使用一个变量来记录当前的前缀和,同时,使用一个哈希表来存储前缀和出现的次数对于当前的元素,计算前缀和,并查看是否存在一个之前的前缀和,使得当前前缀和减去整个前缀和等于k,换句话说,查找currentSum - K 是否在哈希表中如果找到了这样的前缀和,则表示找到了一个和为K的子数组,更新结果计数,将当前前缀和加入哈希表中,以便后续的计算,直到数组遍历结束。由于我们只遍历了一遍数组,所以时间复杂度为o(n)。

完整代码示例:

class Solution {
    public int subarraySum(int[] nums, int k) {
        Map<Integer, Integer> prefixSumCount = new HashMap<>();
        prefixSumCount.put(0, 1); // 前缀和为0的出现次数为1
        int prefixSum = 0; // 当前前缀和
        int count = 0; // 统计和为 k 的子数组个数

        for (int num : nums) {
            prefixSum += num; // 更新当前前缀和
            // 检查是否存在前缀和为 prefixSum - k 的子数组
            if (prefixSumCount.containsKey(prefixSum - k)) {
                count += prefixSumCount.get(prefixSum - k);
            }
            // 更新前缀和出现次数
            prefixSumCount.put(prefixSum, prefixSumCount.getOrDefault(prefixSum, 0) + 1);
        }

        return count;
    }
}

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值