一、LC239. 滑动窗口最大值
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
视频讲解:单调队列正式登场!| LeetCode:239. 滑动窗口最大值_哔哩哔哩_bilibili
暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法。
我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。
大家此时应该陷入深思.....
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列。
不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。
对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。
此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口进行滑动呢?
设计单调队列的时候,pop,和push操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
JAVA
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
MyQueue window = new MyQueue();
int[] result = new int[nums.length - k + 1];
for (int i = 0; i < k; i++) {
window.push(nums[i]);
}
result[0] = window.getMax();
for (int i = k; i < nums.length; i++) {
System.out.println(i-k+1 + " " + nums[i]);
window.poll(nums[i-k]);
window.push(nums[i]);
result[i-k+1] = window.getMax();
}
return result;
}
}
class MyQueue {
Deque<Integer> window = new ArrayDeque<>();
//删除元素
void poll(int val) {
if (!window.isEmpty() && window.peekFirst() == val) {
window.pollFirst();
}
}
//添加元素
void push(int val) {
while (!window.isEmpty() && window.peekLast() < val) {
window.pollLast();
}
window.offerLast(val);
}
int getMax() {
return window.peekFirst();
}
}
Python
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
result = []
queue = deque()
for i in range(k):
while len(queue) > 0 and nums[i] > queue[-1]:
queue.pop()
queue.append(nums[i])
result.append(queue[0])
for j in range(k, len(nums)):
remove = nums[j-k]
if remove == queue[0]:
queue.popleft()
while len(queue) > 0 and queue[-1] < nums[j]:
queue.pop()
queue.append(nums[j])
result.append(queue[0])
return result
- 时间复杂度: O(n)
- 空间复杂度: O(k)
再来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。
有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的O(n)。
其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。
空间复杂度因为我们定义一个辅助队列,所以是O(k)。
二、LC347.前 K 个高频元素
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
先遍历数组,用map统计每个元素出现的频率。
遍历map,维护一个大小为k的小顶堆。最后得到的结果是从小到大排序的前k个高频元素。翻转得到从大到小排序的前k个高频元素。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int i: nums) {
count.put(i, count.getOrDefault(i, 0) + 1);
}
PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
public int compare(Integer a, Integer b){
return count.get(a) - count.get(b);
}
});
for (Integer key: count.keySet()) {
if (pq.size() < k) {//小顶堆元素个数小于k个时直接加
System.out.println(key + " 小于k");
pq.add(key);
} else if (count.get(pq.peek()) < count.get(key)) {
//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
pq.poll();弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
pq.add(key);
System.out.println(key + " 大于k");
}
}
System.out.println(k + " " + pq.size());
int[] results = new int[k];
for (int i = 0; i < k; i++) {//依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
results[i] = pq.poll();
}
return results;
}
}
三、总结
1. 可以出一道面试题:栈里面的元素在内存中是连续分布的么?
这个问题有两个陷阱:
- 陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。
- 陷阱2:缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。