每日算法-250402
这是今天的算法学习记录,主要应用了滑动窗口技巧解决了两道 LeetCode 题目。
LeetCode 2799. 统计完全子数组的数目
问题描述

一个子数组如果包含了数组中所有不同的元素,那么我们称这个子数组是 完全子数组。我们需要统计给定数组中完全子数组的数目。
思路分析
这道题是典型的滑动窗口问题。核心思路是维护一个窗口 [left, right],并统计窗口内不同元素的数量。
- 确定目标:首先,我们需要知道原数组
nums中总共有多少个不同的元素。这可以通过HashSet来统计,记这个数量为n。 - 滑动窗口:
- 我们使用
right指针扩展窗口,将nums[right]加入窗口。 - 同时,我们需要一个机制来跟踪窗口内不同元素的数量
count和每个元素出现的次数(可以使用哈希表或数组hash)。 - 当窗口内不同元素的数量
count等于n时,当前窗口[left, right]就是一个完全子数组。 - 关键点:一旦
[left, right]是完全子数组,那么所有以right为右端点,且左端点l满足0 <= l <= left的子数组[l, right]也一定是完全子数组(因为它们包含了[left, right])。 - 为了找到所有可能的完全子数组,当
count == n时,我们尝试收缩窗口的左边界left。不断将nums[left]移出窗口并更新count,直到count < n。 - 在收缩过程中,每次成功找到一个
count == n的窗口[left, right]时,所有以right为右端点、左端点在[0, left]区间内的子数组都是完全子数组。
- 我们使用
解题过程
- 预处理:使用
HashSet统计nums中不同元素的总数n。 - 初始化:
left = 0:窗口左边界。count = 0:当前窗口内不同元素的数量。ret = 0:完全子数组的总数。hash = new int[2001]:频率数组,用于记录窗口内每个元素的出现次数。
- 遍历 (扩展右边界):使用
for循环遍历nums数组,将当前元素x(即nums[right]) 加入窗口。hash[x]++:增加元素x的计数。if (hash[x] == 1):如果x是第一次加入窗口(计数从 0 变为 1),则count++。
- 收缩左边界 (当窗口满足条件时):
- 使用
while (count == n)循环:只要当前窗口是完全的(包含n个不同元素)。hash[nums[left]]--:尝试将左边界元素nums[left]移出窗口,减少其计数。if (hash[nums[left]] == 0):如果移除后nums[left]的计数变为 0,说明窗口内少了一个不同的元素,count--。left++:将左边界右移。
- 计数:当
while循环因为count < n而结束时,意味着上一步移除nums[left-1]导致窗口不再是完全的。在移除nums[left-1]之前,所有以当前right为右端点、以0到left-1(while循环结束时的left值)为左端点的子数组都是完全的。因此,有left个这样的完全子数组。我们将left加到ret上。
- 使用
- 循环与累加:
for循环继续,right指针右移。每次for循环迭代结束后(无论while循环是否执行),ret += left都会累加上以当前right指针元素为结尾的、满足条件的完全子数组的数量。这里的left值记录了上一次收缩窗口后,能构成合法窗口的最左边界的下一个位置,也就等于以当前right为结尾的合法子数组的数量。 - 返回结果:遍历结束后,
ret即为所求的总数。
复杂度
- 时间复杂度:
O
(
N
)
O(N)
O(N),其中 N 是数组
nums的长度。每个元素最多被left和right指针访问两次。 - 空间复杂度:
O
(
U
)
O(U)
O(U),其中 U 是数组
nums中不同元素的数量(用于HashSet和hash数组)。如果元素值范围固定且不大(如本题的 2000),可以认为是 O ( 1 ) O(1) O(1) 或 O ( m a x _ v a l ) O(max\_val) O(max_val);如果元素值范围很大,HashSet的空间复杂度与不同元素数量 U 相关,最坏情况下是 O ( N ) O(N) O(N)。
Code
class Solution {
public int countCompleteSubarrays(int[] nums) {
int ret = 0;
// 1. 统计数组中不同元素的总数 n
Set<Integer> distinctElements = new HashSet<>();
for (int x : nums) {
distinctElements.add(x);
}
int n = distinctElements.size();
// 2. 初始化滑动窗口变量
int[] freq = new int[2001]; // 频率数组 (根据题目约束,元素值 <= 2000)
int left = 0; // 窗口左边界
int currentDistinctCount = 0; // 当前窗口内不同元素的数量
// 3. 遍历数组,移动右边界 right
for (int right = 0; right < nums.length; right++) {
int currentElement = nums[right];
// 将右边界元素加入窗口
if (freq[currentElement] == 0) {
// 如果是新元素,增加不同元素计数
currentDistinctCount++;
}
freq[currentElement]++;
// 4. 当窗口满足条件 (包含所有 n 个不同元素) 时,尝试收缩左边界
while (currentDistinctCount == n) {
// 在收缩前,[left, right] 是一个完全子数组
// 任何以 right 结尾,以 0..left 开头的子数组都是完全子数组
// 尝试移除左边界元素 nums[left]
int leftElement = nums[left];
freq[leftElement]--;
// 如果移除后,该元素的计数变为 0,则窗口内不同元素数减少
if (freq[leftElement] == 0) {
currentDistinctCount--;
}
// 收缩左边界
left++;
}
// 5. 累加结果
// 退出 while 循环时,left 指向的位置是使得窗口不再是完全子数组的第一个位置
// 因此,以 nums[right] 为右端点的完全子数组的左端点可以是 0, 1, ..., left-1
// 共有 left 个这样的子数组
ret += left;
}
return ret;
}
}
LeetCode 3325. 字符至少出现 K 次的子字符串 I
问题描述

思路分析
同样采用滑动窗口的方法。我们维护一个窗口 [left, right] 和窗口内各字符的频率。
- 维护状态: 使用一个数组
hash记录窗口内'a'到'z'每个字符的出现次数。 - 滑动窗口:
right指针向右移动,将s[right]加入窗口,并更新其频率hash[s[right] - 'a']++。- 检查条件: 当新加入的字符
s[right]的频率 恰好 达到k时 (hash[s[right] - 'a'] == k),说明当前窗口[left, right](以及可能更早的以right结尾的子数组) 不满足 “没有字符频率恰好为 k” 的条件。 - 收缩窗口: 为了恢复满足条件的状态,我们需要移动左边界
left,将s[left]移出窗口,直到s[right]的频率不再是k为止 (或者直到left超过right)。在移动left时,需要同步更新hash[s[left] - 'a']--。 - 计数: 在每次
right指针移动后,并且在可能发生的while循环收缩之后,当前的left值代表了有多少个合法的起始位置。也就是说,对于当前的右端点right,所有以0到left-1为起点的子数组[l, right](其中0 <= l < left) 都是满足 “没有字符频率恰好为 k” 条件的子字符串。因此,将left累加到结果ret中。
解题过程
- 初始化:
left = 0:窗口左边界。ret = 0:满足条件的子字符串总数。hash = new int[26]:字符频率数组。
- 遍历 (扩展右边界):使用
for循环遍历字符串s,设当前字符为c(即s[right])。hash[c - 'a']++:增加字符c的计数。
- 收缩左边界 (当条件不满足时):
- 使用
while (hash[c - 'a'] == k)循环:只要当前加入的字符c的频率等于k。hash[s[left] - 'a']--:将左边界字符s[left]移出窗口,减少其计数。left++:将左边界右移。
- 这个
while循环会一直执行,直到字符c的频率不再是k(通常是因为移除了一个c字符,使得hash[c - 'a']变为k-1),或者left赶上了right(虽然在这个逻辑下不太可能)。
- 使用
- 计数:
- 在
while循环结束后(或者如果从未进入while循环),left指向的位置保证了从0到left-1的所有起始点l构成的子数组[l, right]都不包含频率恰好为k的字符(特别是对于字符c而言,它的频率要么小于k,要么在收缩后小于k)。 ret += left:累加当前右端点right合法子数组的数量。
- 在
- 返回结果:遍历结束后,
ret即为所求的总数。
复杂度
- 时间复杂度:
O
(
N
)
O(N)
O(N),其中 N 是字符串
s的长度。每个字符最多被left和right指针访问两次。 - 空间复杂度: O ( C ) O(C) O(C),其中 C 是字符集的大小(这里是 26)。可以认为是 O ( 1 ) O(1) O(1)。
Code
class Solution {
public int numberOfSubstrings(String ss, int k) {
char[] s = ss.toCharArray();
int ret = 0; // 结果计数
int left = 0; // 滑动窗口左边界
int[] hash = new int[26]; // 字符频率统计 ('a' 到 'z')
// 遍历字符串,移动右边界 right
for (int right = 0; right < s.length; right++) {
char currentChar = s[right];
int charIndex = currentChar - 'a';
// 将当前字符加入窗口,更新频率
hash[charIndex]++;
// 如果当前字符的频率恰好等于 k,说明窗口不满足条件
// 需要收缩左边界,直到该字符频率不再是 k
while (hash[charIndex] == k) {
// 移除左边界字符
hash[s[left] - 'a']--;
// 移动左边界
left++;
}
// 此时,对于当前右边界 right,
// 所有以 [0, left-1] 中任意位置 l 为左边界的子数组 [l, right]
// 都不包含频率恰好为 k 的字符 (特别是 currentChar)。
// 因此,有 left 个这样的合法子数组。
ret += left;
}
return ret;
}
}
