文档讲解: 代码随想录
视频讲解: 《代码随想录》算法公开课-跟着Carl学算法
LeetCode239. 滑动窗口最大值
题目链接:239. 滑动窗口最大值
题目描述:给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值 。
思路:不看随想录的分析根本想不到这种巧妙的思路,而且比较难理解,但是捋清楚之后就会感觉这种思路非常精妙。首先探讨下该题为什么用队列来解决?实际上我们在维护一个窗口并进行滑动的时候,我们需要丢弃最老的数据然后再添加新数据,这就和我们队列的思想很像:先进先出,最先进队列的数据也是最先出队列。然后说下随想录里的思想:
首先需要自定义数组实现一个单调队列,这个队列始终保持单调递减,为什么这么做?其实核心目的就是想让当前队列的头元素始终保持滑窗的最大值,所以当要知道每次滑窗内的最大值时,只需要读取队列的头元素即可。
了解核心思想之后,我们探讨自定义的单调队列,如何设计其offer和poll才能和我们滑窗思想进行结合?需要满足下述条件:
- 当push元素时,若当前所要push的元素大于前面的元素(程序中就是拿当前元素和双端队列的尾部元素进行比较),则需要将它前面的元素弹出,直到队列前面元素都要大于或等于当前元素为止,再push进去当前元素,这样我们才能满足单调递减的要求。大家可能和我当时都会有疑问,为啥这么做?凭什么直接将队列小于当前元素无条件弹出?其实我们可以再斟酌下随想录的解释,队列其实没有必要维护窗口里的所有元素!我们要的是每次滑窗内的最大值,那些小值本来就会随着滑窗的滑动所丢弃,并且没什么用,我们无条件弹出小于当前元素不会带来什么坏的影响,反而会给我们带来更大的方便,哪里方便? 队列的头元素始终维护的是最大值,我们在读取滑窗最大值时直接从队头读就可以了啊,难道这不方便吗?
- 在pop元素时,如果当前要pop的元素不等于当前队列的头元素,则不需要任何处理,为什么?因为这个元素在我们push的时候已经给丢出去了,还处理个毛?所以我们只需要当所pop的元素等于当前队列的头元素时,才执行正常的Pop!
完成上述自定义单调队列之后,我们主函数就非常简单了,我们要做的就是始终保持窗口(队列)大小为要求值,在窗口移动时,一旦超出规定的尺寸大小,我们就需要利用自定义的队列pop出一个老数据,随即利用自定义的push进来一个新数据, 然后我们只需要读当前队列头元素即可求每次窗口内部maxNum!
import java.util.ArrayDeque;
import java.util.Deque;
class MyQueue {
Deque<Integer> deque = new ArrayDeque<>();; // 双端队列
// push函数
public void offer(int value) {
// 当value大于前面元素时(也就是和当前队尾进行比较) 则将前面元素弹出 直到都它小于等于前面元素为止
while (!deque.isEmpty() && value > deque.peekLast()) {
deque.pollLast(); // 弹出队尾元素
}
// 循环判断结束将当前值push进队列
deque.offerLast(value);
}
// pop函数
public void poll(int value) {
// 当所要pop的元素为队头元素时 把队头元素弹出 其余不执行任何操作
if (!deque.isEmpty() && value == deque.peekFirst()) {
deque.pollFirst();
}
}
// 读取窗口内部maxNum
public int peek() {
return deque.peekFirst();
}
}
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// 创建滑窗队列 并始终保持大小为k的滑窗
MyQueue myQueue = new MyQueue();
// 创建存储结果的数组
int[] result = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
if (i < k) {
myQueue.offer(nums[i]); // 当push元素个数小于k时 正常push
if (i == k - 1) {
result[i - k + 1] = myQueue.peek();
}
} else {
// 当元素个数大于k 弹出一个再进一个
myQueue.poll(nums[i - k]);
myQueue.offer(nums[i]);
result[i - k + 1] = myQueue.peek();
}
}
// 循环结束后返回数组
return result;
}
}
LeetCode347.前 K 个高频元素
题目链接:347.前 K 个高频元素
题目描述:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
思路:首先这道题主要分为3个任务:一是统计元素频率;二是对元素频率进行排序 ;三是取前K个频率最高的元素输出。
- 统计元素出现的频率可以采用Map作为数据存储结构,其中key存放元素值,value统计元素频率;
- 可以采用优先级队列(底层是二叉树)对部分元素频率进行排序
- 优先级队列(堆)其底层结构是二叉树,当poll元素时是从树的根节点弹出元素,为了保证在更新堆的时候,弹出的元素始终是频率小的元素,从而保留下来的是频率大的元素,因此我们选择小顶堆(父节点频率始终小于等于左右子树的频率值)。
import java.util.Map;
import java.util.PriorityQueue;
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 创建Map
Map<Integer, Integer> map = new java.util.HashMap<>();
// 循环遍历nums 存到map 并统计出现的频率
for (int num : nums) {
// 优化一下一: getOrDefault()获取指定key对应的value 如果找不到 key ,则返回设置的默认值
map.put(num, map.getOrDefault(num, 0) + 1);
}
// 对出现频率进行排序 创建优先级队列(小顶堆)
// lambda表达式设置优先级队列 o1 - o2 为从小到大
PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
// 遍历map 根据value进行排序 统计频率最高的元素 始终保持堆大小为k
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
// 一旦添加的元素超过k个 则弹出头元素 对应频率小的元素
// 创建二维数组
int[] arr = new int[2];
arr[0] = entry.getKey(); // 元素大小
arr[1] = entry.getValue(); // 元素频率
// 直接添加到优先队列中排序
queue.offer(arr);
// 当大于k时 需要先进排序 再弹出
if (queue.size() > k) {
queue.poll();
}
}
// 循环结束后 当前优先级队列只保存前k个频率最高的元素
// 当前为小顶堆 大值在下面 小值在上面
// 将key值形成数组返回
int[] res = new int[k];
for (int i = k - 1; i >= 0; i--) {
res[i] = queue.poll()[0];
}
return res;
}
}
栈和队列总结
栈模型可以想象成一个顶端开口的瓶子,其特性是先进后出,后进先出。从随想录中针对栈的题目,其实不难发现由于这个后进先出的特性,非常适合元素的匹配问题,比如说LeetCode20. 有效的括号:寻找我们左括号相同类型的右括号,我们直接从栈顶元素去对比;还有LeetCode1047. 删除字符串中的所有相邻重复项:我们在遍历字符串的时候,一旦发现当前所遍历的元素和栈顶元素重复,直接从栈顶pop进而达到删除重复项的目的。LeetCode150. 逆波兰表达式求值:这种表达式为后缀表达式,当遍历到运算符时直接从栈顶pop两个元素进行相应的运算。总之还需要后续做题慢慢去体会。
队列其特性为先进先出,后进后出。利用这种特性与解决涉及滑窗思想的问题非常合适,窗口在进行滑动时,丢弃老数据并添加新数据。解决LeetCode239. 滑动窗口最大值:需要自定义一个单调队列,始终保持队列的头元素是最大值,要想明白我们如何来设计push和poll来符合我们滑窗的思想。虽然这道题是目前做的第一个困难等级,但想明白其实还是很简单的。总之,队列还需要进一步学习,包括优先级队列,大顶堆,小顶堆等,后续都需要再深入了解。