leetcode
leetcode 901 ~ 950
K 个不同整数的子数组

K 个不同整数的子数组

难度:

标签:

题目描述

代码结果

运行时间: 96 ms, 内存: 18.0 MB


/*
 * 思路:
 * 我们使用滑动窗口来找到包含恰好k个不同整数的子数组。
 * 在每一步中,我们将窗口右端扩展并记录每个整数的出现次数。
 * 如果窗口内的不同整数个数大于k,则移动左端以减少不同整数的个数。
 * 最终,当窗口内的不同整数个数正好为k时,记录子数组的数量。
 */
import java.util.*;
import java.util.stream.*;

public class GoodSubarraysStream {
    public long countGoodSubarrays(int[] nums, int k) {
        Map<Integer, Integer> countMap = new HashMap<>();
        int left = 0;
        long result = 0;
        for (int right = 0; right < nums.length; right++) {
            countMap.put(nums[right], countMap.getOrDefault(nums[right], 0) + 1);
            while (countMap.size() > k) {
                countMap.put(nums[left], countMap.get(nums[left]) - 1);
                if (countMap.get(nums[left]) == 0) {
                    countMap.remove(nums[left]);
                }
                left++;
            }
            if (countMap.size() == k) {
                result += right - left + 1;
            }
        }
        return result;
    }

    public static void main(String[] args) {
        GoodSubarraysStream gss = new GoodSubarraysStream();
        System.out.println(gss.countGoodSubarrays(new int[]{1, 2, 1, 2, 3}, 2)); // 输出7
        System.out.println(gss.countGoodSubarrays(new int[]{1, 2, 1, 3, 4}, 3)); // 输出3
    }
}

解释

方法:

这道题要求找出数组中恰好包含 k 个不同整数的子数组数目。题解采用了双指针技术和滑动窗口的方法,结合了两个具体的函数:计算最多有 k 个不同整数的子数组数目的函数 `atMostKDistinct`,以及通过计算 `atMostKDistinct(nums, k) - atMostKDistinct(nums, k-1)` 来得到恰好有 k 个不同整数的子数组数目。这种方法使用了频率数组来跟踪每个元素的出现次数,并动态调整窗口的大小来满足条件。

时间复杂度:

O(n)

空间复杂度:

O(n)

代码细节讲解

🦆
在函数`atMostKDistinct`中,为什么在`nums[right]`的频率为0时,计数器`cnt`会增加?这与算法的哪个部分有关?
在`atMostKDistinct`函数中,当`nums[right]`的频率为0时增加计数器`cnt`是因为每当遇到一个新的整数(即此前未出现过的整数),我们需要增加不同整数的计数。频率为0表示此前该整数没有在当前考虑的窗口中出现过。增加`cnt`反映了窗口内不同整数的增加,这是为了确保我们可以正确追踪和控制窗口中包含的不同整数的数量,符合函数的目标——计算至多包含k个不同整数的子数组数目。
🦆
如何理解和计算`res += right - left`这一步骤中的`right - left`?它代表了什么意义?
`right - left`代表的是当前有效窗口的长度。在滑动窗口方法中,每当我们扩展右指针`right`以包括一个新的元素,并且窗口内的不同整数的数量符合条件时(即不超过k个),窗口从左指针`left`到右指针`right`之间的任何一个子数组都是有效的。因此,`right - left`实际上表示以`nums[right]`结尾的、符合条件的子数组的数量,这些子数组的起始位置可以从`left`到`right`的任何位置。这一步骤通过累加这些可能的子数组数目来更新结果`res`。
🦆
在处理边界条件时,如何确保`left`指针不会超过`right`指针,特别是在右指针开始移动时?
在`atMostKDistinct`函数的实现中,右指针`right`总是先于左指针`left`移动,这确保了在任何调整左指针之前,右指针已经至少向前移动了一步。这种先移动右指针扩展窗口,再移动左指针缩小窗口的策略,确保了`left`指针永远不会超过`right`指针。此外,左指针的移动是在一个内部的循环中进行的,该循环只有在`cnt`(窗口中的不同整数数量)超过k时才会执行,并且循环的条件是`cnt > k`,这保证了左指针的移动是有条件和有限的,避免了越过右指针。
🦆
在减少`freq[nums[left]]`后,为什么要检查`freq[nums[left]]`是否变为0来决定是否减少`cnt`?
在减少`freq[nums[left]]`后检查是否变为0是因为我们需要准确追踪窗口内不同整数的数量。`freq[nums[left]]`表示整数`nums[left]`在窗口中的频率,当这个频率降至0时,意味着该整数已经完全从窗口中移除,因此不应再被计入窗口内不同整数的总数。于是,我们需要减少`cnt`以反映窗口内不同整数数量的减少。这是确保`cnt`准确反映当前窗口状态的关键步骤。

相关问题

无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

 

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

 

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

至多包含两个不同字符的最长子串

至多包含两个不同字符的最长子串

至多包含 K 个不同字符的最长子串

至多包含 K 个不同字符的最长子串