滑动窗口最大值
思路
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列
对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。
此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口进行滑动呢?
设计单调队列的时候,pop,和push操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
代码
class MyQueue {
Deque<Integer> deque = new LinkedList<>();
//弹出元素时,比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出
//同时判断队列当前是否为空
void poll(int val) {
if (!deque.isEmpty() && val == deque.peek()) {
deque.poll();
}
}
//添加元素时,如果要添加的元素大于入口处的元素,就将入口元素弹出
//保证队列元素单调递减
//比如此时队列元素3,1,2将要入队,比1大,所以1弹出,此时队列:3,2
void add(int val) {
while (!deque.isEmpty() && val > deque.getLast()) {
deque.removeLast();
}
deque.add(val);
}
//队列队顶元素始终为最大值
int peek() {
return deque.peek();
}
}
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];
int num = 0;
//自定义队列
MyQueue myQueue = new MyQueue();
//先将前k的元素放入队列
for (int i = 0; i < k; i++) {
myQueue.add(nums[i]);
}
res[num++] = myQueue.peek();
for (int i = k; i < nums.length; i++) {
//滑动窗口移除最前面的元素,移除是判断该元素是否放入队列
myQueue.poll(nums[i - k]);
//滑动窗口加入最后面的元素
myQueue.add(nums[i]);
//记录对应的最大值
res[num++] = myQueue.peek();
}
return res;
}
}
解释一下Dequeue:
在Java中,
Queue
和Deque
是两种不同类型的集合,它们都用于以特定顺序存储元素,但在功能和使用方式上有所不同。Queue
Queue
接口在Java集合框架中定义,代表了一个先进先出(FIFO)的队列。队列通常用于在处理前按照顺序保持元素。Queue
接口提供了基本的队列操作方法,如offer
(向队列添加元素)、poll
(移除并返回队列头部的元素)、peek
(返回队列头部的元素但不移除)等。Deque
Deque
(双端队列)接口扩展了Queue
接口,提供了更灵活的队列操作,允许元素从队列的两端被添加或移除,即它支持先进先出(FIFO)和后进先出(LIFO)两种模式。Deque
接口提供了一系列方法,包括addFirst
、addLast
、offerFirst
、offerLast
、removeFirst
、removeLast
、pollFirst
、pollLast
、getFirst
、getLast
等,使得它既可以作为标准队列使用,也可以作为栈使用。主要区别
- 功能范围:
Deque
接口提供了比Queue
接口更广泛的功能,支持从两端插入和移除元素。- 使用场景:由于
Deque
支持更灵活的元素操作,它可以被用于更多场景,如当作栈使用(后进先出)或者标准队列(先进先出)。- 方法差异:
Deque
接口包含了Queue
接口的所有方法,并且添加了一些额外的方法来支持双端操作。在选择使用
Queue
还是Deque
时,应考虑所需的数据结构操作类型。如果需要标准的队列操作,Queue
就足够了;如果需要从两端操作数据,或者需要一个可以同时支持队列和栈操作的数据结构,那么Deque
会是更好的选择。
前k个高频元素
思路
这道题目主要涉及到如下三块内容:
- 要统计元素出现频率
- 对频率排序
- 找出前K个高频元素
首先统计元素出现的频率,这一类的问题可以使用map来进行统计。
然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列。
什么是优先级队列呢?
其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢?
缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。
什么是堆呢?
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。
本题我们就要使用优先级队列来对部分频率进行排序。
为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。
此时要思考一下,是使用小顶堆呢,还是大顶堆?
有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。
那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。
而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢?
所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)
代码
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 优先级队列,为了避免复杂 api 操作,pq 存储数组
// lambda 表达式设置优先级队列从大到小存储 o1 - o2 为从小到大,o2 - o1 反之
PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]);
int[] res = new int[k]; // 答案数组为 k 个元素
Map<Integer, Integer> map = new HashMap<>(); // 记录元素出现次数
for(int num : nums) map.put(num, map.getOrDefault(num, 0) + 1);
for(var x : map.entrySet()) { // entrySet 获取 k-v Set 集合
// 将 kv 转化成数组
int[] tmp = new int[2];
tmp[0] = x.getKey();
tmp[1] = x.getValue();
pq.offer(tmp);
// 下面的代码是根据小根堆实现的,我只保留优先队列的最后的k个,只要超出了k我就将最小的弹出,剩余的k个就是答案
if(pq.size() > k) {
pq.poll();
}
}
for(int i = 0; i < k; i ++) {
res[i] = pq.poll()[0]; // 获取优先队列里的元素
}
return res;
}
}
我们不管是什么元素 先把他都offer进来 再poll他会自动poll最小的 因为我们采取的是优先级队列
这里的var x也可以替换成Map.Entry<Integer,Integer> entry
总结
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
栈往往不被归类为容器,而被归类为container adapter(容器适配器)。 默认为deque
在Java中,栈(Stack)和队列(Queue)这两种数据结构的底层实现方式多样,主要依赖于Java集合框架(Java Collections Framework)提供的类和接口。下面是一些常见的实现方式:
栈的实现
Java中的栈可以通过java.util.Stack
类实现,也可以使用Deque
接口的实现类,如ArrayDeque
,来模拟栈的行为。java.util.Stack
类本身继承自Vector
,因此它的实现是基于数组的,但是由于Stack
是一种遗留类,通常推荐使用更现代的ArrayDeque
来实现栈的功能。
-
java.util.Stack:
- 底层基于动态数组实现。
- 提供了标准的栈操作,例如
push
(入栈)、pop
(出栈)、peek
(查看栈顶元素)等。 - 由于继承自
Vector
,它是同步的(线程安全),但在单线程环境下可能导致不必要的性能开销。
-
java.util.ArrayDeque:
- 底层使用循环数组实现。
- 可以高效地从两端添加或移除元素,非常适合作为栈使用。
- 不是线程安全的,但在单线程环境中性能优于
Stack
。 - 使用
push
、pop
和peek
方法来模拟栈操作。
队列的实现
队列在Java中通常是通过java.util.Queue
接口实现的,这是一个继承自java.util.Collection
的接口。Queue
接口有多种实现,包括LinkedList
、ArrayDeque
、PriorityQueue
等,每种实现有其特定的用途。
-
java.util.LinkedList:
- 同时实现了
List
和Deque
接口。 - 底层基于双向链表实现。
- 适用于需要频繁进行元素插入和删除操作的场景。
- 同时实现了
-
java.util.ArrayDeque:
- 底层使用循环数组实现。
- 既可以作为队列使用,也可以作为栈使用。
- 适用于需要高效随机访问的场景。
-
java.util.PriorityQueue:
- 底层基于堆结构(通常是二叉堆)实现,用于创建优先级队列。
- 元素按照其自然顺序或者构造时提供的
Comparator
进行排序。 - 不保证同等优先级元素的顺序。
总结
Java中的栈和队列可以通过不同的类实现,选择合适的实现取决于具体的应用需求。例如,如果需要线程安全的栈实现,可以选择Stack
;如果需要高效的非同步栈实现,可以使用ArrayDeque
。对于队列,LinkedList
适合频繁的插入删除操作,ArrayDeque
适合大量的随机访问操作,而PriorityQueue
适用于需要元素排序的场景。