代码随想录算法训练营第13天 | 栈与队列part03 239. 滑动窗口最大值 347.前 K 个高频元素
题目一 239. 滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:在线性时间复杂度内解决此题。
关键思路:自己创建一个子队列,维持这个队列保持单调递增或者递减。
比如:让出口处元素保持最大,后面的元素依次递减,那么就在传进元素时,不断留下更大的,抛出小的,直到后面进入队列的元素没有前者大为止。
然后如果出现了更大的元素,就直接推掉它前面的全部元素(因为之前的最大值都没有它大),然后再让后面的进来。
效果图如下所示。
队列中依次进入2,3,5,就从前面推掉2,3只留下5;
后面进入1,4,4没5大,但比1大,从后端把1推掉,5,4留在队列中;
8进来,比前面的都大(比前面最大的5还大),把前面的全推掉,队列里只剩下8.
注意:本题并不止返回最终的最大值,需要对整个过程中出现的极大值进行记录,并打包在一个数组中。数组的长度也是整个数据数组和滑动窗口组合的可能性数量,为 nums.length - (k-1).
比如数组长为8,滑动窗口长度为3,那么滑动过程中的情况共6种,因此也会出现6种局部最大值。
注意:pop的时候进行判断,如果当前要扔出的是队列最前面,也是最大的元素,会为它单独调用pop()让它出去;此时后面的元素不动。直到再次push时,再判断扔出前面的元素,留下最大的在前端。
如果新进来的元素比最前端最大的元素小,但比它本身前面的元素大,那么它将依次将这些小元素向后扔掉。
import java.util.*;
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 1)
return nums;
int len = nums.length - (k - 1);
int[] res = new int[len];//最终答案的数组
Myqueue que = new Myqueue(); //队列
int count = 0;
for(int i=0; i<k; i++)//先放进k个然后比
{
que.push(nums[i]);
}
res[count] = que.Getmax();
count++;
for(int i=k; i<nums.length; i++)//从后面挨个塞挨个比较
{
que.pop( nums[i - k]);//注意这里是排掉上面循环中队列元素
que.push( nums[i] );
res[count] = que.Getmax();
count++;
}
return res;
}
}
class Myqueue{
Deque<Integer> newque = new LinkedList<>();
public void push(int val)
{
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
while( newque.isEmpty() != true && val > newque.getLast() )//进来的值比末尾这个大
{
newque.removeLast();//把最后一个元素推了
}
newque.add(val);
}
public void pop(int val)
{//队列的peek()处为最大值,也是出口,也是last端
if( newque.isEmpty() != true && val == newque.peek() )
{
newque.poll();//为最大值单独调用poll
}
}
public int Getmax()
{
return newque.peek();
}
}
题目二 347.前 K 个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 :
- 输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
本题考虑使用优先级队列或者堆(节点具有权值且按序排列的完全二叉树)来解决。
因为返回出现频率前k 高的元素,因此只需维护一个节点数为k的顶堆,不断向堆中push和pop即可。
那么是用大顶堆还是小顶堆?因为顶堆的插入从叶子节点处,而删除却只能从根节点处,所以应该采用小顶堆,不断从根处删除最小的元素,从叶子处插入最大的。
不过在这之前,还需要使用哈希表。其中 key存储数组元素值,value为对应的元素出现次数。
时间复杂度: O(nlogk)
空间复杂度: O(n)
c++版本如下:
c++版本的好处是可以重载括号(),不过和java版本一样使用了优先级队列和哈希表。
class Solution {
public:
// 小顶堆
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
// 要统计元素出现频率
unordered_map<int, int> map; // map<nums[i],对应出现的次数>
for (int i = 0; i < nums.size(); i++) {
map[nums[i]]++;
}
// 对频率排序
// 定义一个小顶堆,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;
// 用固定大小为k的小顶堆,扫面所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
pri_que.push(*it);
if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
pri_que.pop();
}
}
// 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
vector<int> result(k);
for (int i = k - 1; i >= 0; i--) {
result[i] = pri_que.top().first;
pri_que.pop();
}
return result;
}
};
java版本的答案使用了优先级队列,lambda函数,HashMap以及Map.Entry,挨个理解起来和使用真的很复杂,不如c++版本的清楚明朗…
import java.util.*;
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> newmap = new HashMap<>();
for(int i : nums)
{
newmap.put( i, newmap.getOrDefault(i, 0) + 1);
}
//在优先队列中存储二元组(num,cnt),cnt表示元素值num在数组中的出现次数
//出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>( ( pair1, pair2)-> pair1[1]-pair2[1] );//lambda函数
for( Map.Entry<Integer,Integer> entry : newmap.entrySet()){//小顶堆只需要维持k个元素有序
if(pq.size() < k){//小顶堆元素个数小于k个时直接加
pq.add(new int[]{entry.getKey(),entry.getValue()});
}else{
if(entry.getValue() > pq.peek()[1]){//当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
pq.poll();//弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
pq.add(new int[]{ entry.getKey() ,entry.getValue() });
}
}
}
int[] ans = new int[k];
for(int i=k-1; i>=0; i--){//依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
ans[i] = pq.poll()[0];
}
return ans;
}
}