目录
【239. 滑动窗口最大值】- 第1道困难题
方法一 暴力解法,超时
提交结果:49 / 51 个通过的测试用例,还剩2个没运行完就超时了
总结:做了一定的优化,即如果上一个滑动窗口的最大值不是上一个滑动窗口最左边的值,就只需要将上一个滑动窗口的最大值(交集的最大值)与当前滑动窗口最右边的值进行比较,两者更大的值就是当前滑动窗口的最大值。但是,最坏的情况下,依然是O(n*k)的时间复杂度。
class Solution {
public int getLeftMax(int[] nums, int start, int k, int max){
int leftMax;
if (max > nums[start]){
return max;
}
else{
leftMax = nums[start + 1];
for (int i = start + 1; i < start + k; i++){
if (nums[i] > leftMax) leftMax = nums[i];
}
}
return leftMax;
}
public int[] maxSlidingWindow(int[] nums, int k) {
// 返回的数组长度为 nums.length - k + 1
int[] res = new int[nums.length - k + 1];
// 获取第一个窗口的最大值
res[0] = nums[0];
for (int i = 0; i < k; i++){
if (nums[i] > res[0]) res[0] = nums[i];
}
// 将上个窗口的最大值和当前窗口的新值比较,获取当前窗口的最大值
for (int i = 1; i <= nums.length - k; i++){
// 获取当前窗口的最右边的值(新值)
int newValue = nums[i+k-1];
// 获取上一个窗口和当前窗口交集的最大值
int leftMax = getLeftMax(nums, i-1, k, res[i-1]);
// 新值与交集的最大值比较,取更大的作为当前窗口最大值
res[i] = Math.max(leftMax, newValue);
}
return res;
}
}
方法二 使用自定义规则的双端队列实现
思路:利用双端队列实现,队列只存可能为最大值的值,队头始终是最大值
1、设定双端队列的规则
队头始终是队列的最大值,队列始终是降序排序的。
2、利用push(int x)方法维护队列规则
需要先将队列中x前面比x小的值弹出,再将x压入队列。
3、利用for循环遍历数组中的每个元素
- 当还没遍历到k个值时,只需要将遍历到的元素压入队列
- 当遍历到第k个值时,还要记录当前滑动窗口的最大值,弹出窗口最左边的值
注意:
当前窗口最左边的值很可能在【将当前遍历元素push进队列】的时候就已经弹出(维护规则所需),因此要加判断条件。
- 如果队列的第一个值不是当前窗口最左边的值,则证明之前已经被弹出;
- 如果队列的第一个值是当前窗口最左边的值,则需要弹出。
附上卡哥的视频讲解【单调队列正式登场!| LeetCode:239. 滑动窗口最大值】
class Solution {
// 队列deque维护的规则是,始终队头的元素是最大的,队头到队尾是降序的
Deque<Integer> deque = new ArrayDeque<>();
// 将当前窗口的新值压入队列
public void push(int x){
// 1.将队列中x前面比x小的值弹出
while (!deque.isEmpty() && deque.getLast() < x) deque.removeLast();
// 2.将x压入队列
deque.addLast(x);
}
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
int num = 0;
for(int i = 0; i < nums.length; i++){
// 当还没遍历到k个值时,只需要将遍历到的元素压入队列
push(nums[i]);
// 当遍历到第k个值时,就要记录当前滑动窗口的最大值,弹出窗口最左边的值
if (i >= k - 1){
res[num++] = deque.getFirst(); // 记录最大值,由于队头始终最大,获取队头即可
if (deque.getFirst() == nums[i-k+1]) deque.removeFirst(); // 若最左边的值还在队列中,则弹出
}
}
return res;
}
}
时间复杂度: O(n),所有操作相当于n个元素分别被push和pop一次,因此还是O(n)
空间复杂度: O(k),最坏的情况下,将滑动窗口内k个元素都push进队列
【347. 前K个高频元素】
思路:Map(统计频次)+PriorityQueue(构建大小为k的小顶堆,对频次进行排序)
1、计算各元素出现的频率表map,key为元素值,value为出现的频次
2、创建小顶堆,指定排序方式(升序:o1-o2 降序:o2-o1)
传入的是数组,第一个是整数,第二个是整数出现的频次
必须传入数组,因为虽然排序的是频次,但还要记录频次对应的整数是哪个,用于返回结果
3、遍历map,利用大小为k的小顶堆获取频率前k高的元素
4、返回结果,注意要将小顶堆弹出的值倒序存入res(res是从大到小,频次前k高的整数)
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 1、计算各元素出现的频率表map,key为元素值,value为出现的频次
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
// 2、创建小顶堆,指定排序方式(升序:o1-o2 降序:o2-o1)
// 传入的是数组,第一个是整数,第二个是整数出现的频次
// 必须传入数组,因为虽然排序的是频次,但是还要记录频次对应的整数是哪个,用于返回结果
PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]); // 小顶堆是升序
// 3、遍历map,利用大小为k的小顶堆获取频率前k高的元素
for (Integer key: map.keySet()){
int[] pair = new int[2];
pair[0] = key; // pair的第一个元素是整数
pair[1] = map.get(key); // pair的第二个元素是整数出现的频次
pq.offer(pair); // 将pair加入堆进行排序,调整小顶堆
// 如果小顶堆的大小大于k,就将最小的堆顶元素弹出,始终保持堆的大小为k
if (pq.size() > k){
pq.poll();
}
}
// 4、返回结果
int[] res = new int[k];
for(int i = k - 1; i >= 0; i--){
res[i] = pq.poll()[0]; // 获取的是pair中的第一个元素,即整数
}
return res;
}
}
时间复杂度: O(nlogk),遍历map是O(n),遍历过程还要维护小顶堆O(logk),相乘得O(nlogk)
空间复杂度: O(n),如果nums中每个元素值都不相同,那么map就是O(n)
【栈与队列总结】
一、利用Deque实现栈和队列(官方推荐)
1、创建Deque对象
ArrayDeque是基于数组实现的双端队列,而LinkedList是基于链表实现的双端队列。
- Deque<> deque = new ArrayDeque<>(); // 基于数组实现
- Deque<> deque = new LinkedList<>(); // 基于链表实现
2、Deque接口定义的常用方法
- 针对<栈>的方法:push()和pop()
- 针对<队列>的方法:offer()和poll()
二、优先级队列PriorityQueue的使用
1、利用PriorityQueue构建小顶堆/大顶堆的方式
PriorityQueue<int> pq = new PriorityQueue<>((o1, o2) -> o1 - o2); // 升序 -> 小顶堆
PriorityQueue<int> pq = new PriorityQueue<>((o1, o2) -> o2 - o1); // 降序 -> 大顶堆
2、堆压入元素/弹出元素
pq.offer(value); // 压入元素,自动调整堆的排序
pq.poll(); // 弹出堆顶元素(最小值/最大值)
3、获取堆的大小
int size = pq.size();