朝阳做网站的公司南宁百度首页优化
这是今天的算法学习记录,主要应用了滑动窗口技巧解决了两道 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. 统计数组中不同元素的总数 nSet<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. 遍历数组,移动右边界 rightfor (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')// 遍历字符串,移动右边界 rightfor (int right = 0; right < s.length; right++) {char currentChar = s[right];int charIndex = currentChar - 'a';// 将当前字符加入窗口,更新频率hash[charIndex]++;// 如果当前字符的频率恰好等于 k,说明窗口不满足条件// 需要收缩左边界,直到该字符频率不再是 kwhile (hash[charIndex] == k) {// 移除左边界字符hash[s[left] - 'a']--;// 移动左边界left++;}// 此时,对于当前右边界 right,// 所有以 [0, left-1] 中任意位置 l 为左边界的子数组 [l, right]// 都不包含频率恰好为 k 的字符 (特别是 currentChar)。// 因此,有 left 个这样的合法子数组。ret += left;}return ret;}
}