LeetCode 239. 滑动窗口最大值
暴力解法超时,代码如下:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Queue<Integer> queue = new ArrayDeque<>();
int left = 0;
int right = k - 1;
while(right < nums.length){
int max = nums[left];
for(int i=left; i<=right; i++){
if(nums[i] > max){
max = nums[i];
}
}
queue.offer(max);
left++;
right = left + k - 1;
}
int[] results = new int[queue.size()];
int index = 0;
while(queue.size() != 0){
results[index++] = queue.poll();
}
return results;
}
}
看题解文章发现看不明白,于是去听了视频讲解,大致思路:定义一个单调栈,只维护可能成为最大值的元素,队列为单调增的,保证队列的头一定是最大的。当要push一个元素进队列前,先检查当前队列中是否有比该元素小的,若有则将这些元素弹出,由于该队列是单调增的,所以可以从最后一个元素开始比较,知道队列中的元素都比该元素大,则将该元素加入队列中;当要pop一个元素时,只需判断该元素是否等于当前头部的元素,若等于,则将头部元素弹出,若不等于则无需进行操作,因为头部元素之前的队列都已被弹出;当要访问当前队列的最大值时,直接返回头部元素。
该题的关键在于使用deque数据结构,即双端队列 double ended queue,其既可头尾添加元素,又可头尾删除元素,使用起来非常方便。
该题中滑动窗口的移动与队列的pop和push操作十分类似,下次遇到这种题要反应过来的。
代码如下:
class MyQueue {
Deque<Integer> queue = new ArrayDeque<>();
void pop(int val){
if(queue.size() > 0 && val == queue.peek()) {
queue.poll();
}
}
void push(int val) {
while(queue.size() > 0 && queue.getLast() < val){
queue.removeLast();
}
queue.offer(val);
}
int getMax() {
return queue.peek();
}
}
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 1) return nums;
MyQueue queue = new MyQueue();
int[] result = new int[nums.length - k + 1];
int count = 0;
for(int i=0; i<k; i++){
queue.push(nums[i]);
}
result[count++] = queue.getMax();
for(int i=k; i<nums.length; i++){
queue.pop(nums[i-k]);
queue.push(nums[i]);
result[count++] = queue.getMax();
}
return result;
}
}
LeetCode 347.前 K 个高频元素
这道题完全是看题解做出来的,就当是学习笔记。
该题主要涉及三块内容:
1.统计各数出现频率;
2.按照频率进行排序;
3.得到频率前k个数;
对于第一块,由于要同时记录数和频率,所以应马上想到使用Map来做统计。
对于第二块,涉及到我应该学习的新知识,有个数据结构叫PriorityQueue,是一种基于优先级堆的无界优先级队列(百度说的,没明白),它是一个队列,其中每个元素都被赋予了一个优先级。具有较高优先级的元素先被处理,而具有较低优先级的元素后被处理。同等优先级的元素按照它们被添加到队列的顺序进行处理。
什么是堆呢?
堆是一颗完全二叉树,树中每个节点的值都不大于(或不小于)其左右孩子的值。如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
此时要思考一下,是使用小顶堆呢,还是大顶堆?
若定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢?
所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
对于第三块,将最后得到的小顶堆依次弹出,最先弹出的出现的次数最少,后面的会依次增加。
代码如下:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
// 1.统计元素出现频率
for(int num:nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
// 2.对频率排序
// 使用优先队列PriorityQueue对出现次数进行升序排序(小顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
for(Map.Entry<Integer,Integer> entry:map.entrySet()) {
if(pq.size() < k){
pq.add(new int[]{entry.getKey(), entry.getValue()});
} else {
// 3.找出前K个高频元素
if(entry.getValue() > pq.peek()[1]) {
pq.poll(); // 弹出小顶堆的头部,即k个元素中最小的
pq.add(new int[]{entry.getKey(), entry.getValue()}); // 将新元素加进来
}
}
}
int[] result = new int[k];
for(int i=k-1; i>=0; i--){
result[i] = pq.poll()[0];
}
return result;
}
}
在Java中,PriorityQueue默认是按照升序排序的。当我们传入一个Lambda表达式作为比较器时,这个比较器决定了队列中元素的排序方式。在这个例子中,Lambda表达式(pair1, pair2) -> pair1[1] - pair2[1]表示,优先队列会根据每个元素的第二个元素进行升序排序。
如果要按照降序排序,我们可以将Lambda表达式修改为(pair1, pair2) -> pair2[1] - pair1[1]。这样,优先队列就会根据第二个元素进行降序排序。
总结
灵魂四问:
1.Java中stack,queue 是容器么?
答:Java中的Stack和Queue是容器,它们是Java集合框架的一部分,
Stack是一种后进先出(LIFO)的数据结构,Java中提供了Stack类来实现这种数据结构。
Queue是一种先进先出(FIFO)的数据结构,Java中提供了Queue接口以及一些实现类,如LinkedList和PriorityQueue,来实现这种数据结构。
这些容器类都是Java集合框架的一部分,该框架是一组用于存储和管理数据的类和接口的集合。除了Stack和Queue之外,Java集合框架还包括其他类型的容器,如List、Set、Map等。
2.Java中stack,queue是如何实现的?
对于Stack类,继承自Vector,它的实现比较简单,底层实际上还是数组,主要是通过一个数组来存储元素。每个栈帧(包括对象和对应的描述符)都存储在一个数组的元素中。
对于Queue接口,Java提供了多种实现方式,如LinkedList、ArrayDeque、PriorityQueue等。这些实现类都是基于链表或数组来实现的。以LinkedList为例,它实现了Queue接口,通过维护一个链表结构来实现元素的存储和取出。当一个元素被入队时,将其添加到链表的末尾;当元素被出队时,从链表头部删除该元素。
3.stack,queue 提供迭代器来遍历空间么?
答:Java中的Stack类和Queue接口都提供了迭代器来遍历其元素。由于Stack和Queue是基于线性数据结构的,因此它们的迭代器是按顺序访问元素的。在遍历过程中,不能对元素进行随机访问,只能按照特定的顺序进行遍历。
对于Stack类,可以通过调用iterator()
方法来获取一个迭代器。该迭代器可以遍历栈中的元素,按照它们被压入栈的顺序进行访问。
对于Queue接口,不同的实现类提供的方法可能会有所不同。例如,LinkedList类提供了iterator()
方法来获取一个迭代器,而ArrayDeque类提供了iterator(),descendingIterator()
方法来获取迭代器和反向迭代器。这些迭代器可以按照特定的顺序遍历队列中的元素。