一 LC76.最小覆盖子串
题目要求:
给你一个字符串
s
、一个字符串t
。返回s
中涵盖t
所有字符的最小子串。如果s
中不存在涵盖t
所有字符的子串,则返回空字符串""
。注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。- 如果
s
中存在这样的子串,我们保证它是唯一的答案。
思路分析:
要解决这个问题,可以使用滑动窗口技术结合字符计数。基本思路是通过一个窗口在字符串 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
个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值
思路分析:
我们可以使用双端队列来完成,设置双端队列存储最大值(便于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
的子数组的个数 。子数组是数组中元素的连续非空序列。
思路分析:
做了两道困难题,做一道中等题目休息一下吧。和为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;
}
}