目录
单调队列
单调队列(Monotonic Queue)是一种特殊的队列结构,通常用于在滑动窗口、最大最小值等问题中实现高效的查询操作。以下是单调队列的基本概念及其在相关问题中的应用总结:
1. 单调队列的定义
- 单调递增队列:队列中的元素按值递增排序。当我们向队列中添加一个新元素时,所有比它大的元素都会被移除,以保持队列的单调性。及最后一个元素最大
- 单调递减队列:队列中的元素按值递减排序。当我们向队列中添加一个新元素时,所有比它小的元素都会被移除,以保持队列的单调性。及最后一个元素最小
2. 单调队列的操作
- 入队 (enqueue):插入新元素时,根据单调性移除不符合条件的元素。
- 出队 (dequeue):根据需要从队列头部移除元素,通常与滑动窗口的左边界移动有关。
- 保持单调性:通过在入队时移除不符合单调性的元素,队列始终保持单调性。
3. 单调队列的应用
- 滑动窗口中的最大/最小值问题:在给定数组中找到每个长度为
k
的子数组的最大或最小值。单调队列可以在 O(n) 时间内完成此操作,其中 n 是数组的长度。 - 动态维护区间最值:单调队列能够高效地维护区间中的最大值或最小值,适用于需要频繁更新区间的场景。
4. 单调队列的优势
- 高效性:通过维护队列的单调性,可以在常数时间内获得当前区间的最值,并且只需线性时间复杂度遍历整个数组。
- 空间效率:队列的空间复杂度通常为 O(k),其中 k 是滑动窗口的大小。
5. 实现细节
- 数据结构:可以使用双端队列(
Deque
)来实现单调队列,双端队列支持在队列两端进行高效的插入和删除操作。 - 入队操作:插入新元素时,从队尾逐一比较并移除不符合单调性的元素,然后将新元素插入队尾。
- 出队操作:在滑动窗口问题中,当窗口左边界向右移动时,如果当前最大或最小值已不在窗口范围内,则从队列头部移除它。
150.逆波兰表达式求值
1.解题思路
把元素放入栈中,遇到操作符后,将最后两个元素拿出来操作,最后一个作为右操作符,前一个作为左操作符,然后再把操作结果放入进去。整个后缀表达式的结果就是栈中最后一个元素。
2.我的实现
实现遇到的问题
对于break的使用
1.在switch
语句中:break
只终止当前case
的执行,跳出switch
语句。
2.在循环结构中:break
终止整个循环。
将字符转换成int
Integer.parseInt(tokens[i])
的作用就是将位于 tokens
数组第 i
个位置上的字符串转换为整数。
代码
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
int num1,num2;
for (int i = 0; i < tokens.length; i++) {
switch (tokens[i]) {
case "+":
num2=stack.pop();
num1=stack.pop();
stack.push(num1+num2);
break;
case "-":
num2=stack.pop();
num1=stack.pop();
stack.push(num1-num2);
break;
case "*":
num2=stack.pop();
num1=stack.pop();
stack.push(num1*num2);
break;
case "/":
num2=stack.pop();
num1=stack.pop();
stack.push(num1/num2);
break;
default:
stack.push(Integer.parseInt(tokens[i]));//将字符转换成int类型
break;
}
}
return stack.peek();
}
代码中的错误
3.标准实现
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList();
for (String s : tokens) {
if ("+".equals(s)) { // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
stack.push(stack.pop() + stack.pop()); // 注意 - 和/ 需要特殊处理
} else if ("-".equals(s)) {
stack.push(-stack.pop() + stack.pop());
} else if ("*".equals(s)) {
stack.push(stack.pop() * stack.pop());
} else if ("/".equals(s)) {
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 / temp1);
} else {
stack.push(Integer.valueOf(s));
}
}
return stack.pop();
}
}
区别
4.题目总结
栈
- 定义:堆栈是一种后进先出 (LIFO) 的数据结构,适合处理逆波兰表示法表达式的计算。
- 操作:在遇到操作数时,将其压入栈中;在遇到操作符时,从栈中弹出相应数量的操作数,进行计算后将结果再压入栈中。
- 用途:在逆波兰表达式计算中,堆栈用于存储操作数和中间计算结果。
239. 滑动窗口最大值
1.解题思路
用一个双端队列,每移动一个元素就pop最后一个,push最新的一个,然后再获得最大值。
pop保持队列出口出是最大的一个元素,当进入的比前面大时,就把前面的pop出去
小的话就可以放进来,因为我们维护的是出口处的元素。
相当于这个队列中只储存两个值一个是最大值储存到第一位,一个是最后一个为最大值,储存在第二位,相等时接着后面储存
2.我的实现
实现遇到的问题
1.ArrayDeque
和 LinkedList用哪个实现
Deque更好:
ArrayDeque
是滑动窗口问题中实现双端队列的最佳选择。它能够提供高效的双端操作,确保滑动窗口在处理过程中能够快速、稳定地完成元素的插入、删除和查询。
2.注意避免重复判断,重复加入会引起错误,学会用else。
代码
public static int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> deque = new ArrayDeque<>();
int[] result = new int[nums.length - k+1];
for (int i = 0; i < nums.length; i++) {
if (deque.isEmpty()){
deque.addLast(nums[i]);
}
else if (nums[i] > deque.getFirst()) {//如果大于了最前面的数
// 那所有数都要移走
while (!deque.isEmpty()) {
deque.pollLast();
}
deque.addLast(nums[i]);
}else {
while (deque.peekLast()<nums[i]){
deque.pollLast();
}
deque.addLast(nums[i]);
int a=nums[i];
}
if (i >= k - 1) {
result[i - k + 1] = deque.getFirst();
//以窗口最后一个数为i,第一个数为i-k+1
//当最大数等于窗口第一个数时,意味着要移除
if (deque.getFirst() == nums[i - k + 1]) {
deque.pollFirst();
}
}
}
return result;
}
3.标准实现
//解法一
//自定义数组
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;
}
}
//解法二
//利用双端队列手动实现单调队列
/**
* 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
* 单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (--> head) (右边为头结点,元素存的是下标)
*/
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
ArrayDeque<Integer> deque = new ArrayDeque<>();
int n = nums.length;
int[] res = new int[n - k + 1];
int idx = 0;
for(int i = 0; i < n; i++) {
// 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点
// 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
while(!deque.isEmpty() && deque.peek() < i - k + 1){
deque.poll();
}
// 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offer(i);
// 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
if(i >= k - 1){
res[idx++] = nums[deque.peek()];
}
}
return res;
}
}
4.题目总结
解题思路里
347.前 K 个高频元素(!!!)
1.解题思路
用map,key来存放元素,value来存放次数
用value排序,取数量前k个的key值
大顶堆和小顶堆特别擅长在一个很大的数据集里求前k个高频或低频元素
2.我的实现
实现遇到的问题
代码
public int[] topKFrequent2(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>(); //key为数组元素值,val为对应出现次数
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
//在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
//出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
for (Map.Entry<Integer, Integer> entry : map.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;
}
3.标准代码
public int[] topKFrequent2(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>(); //key为数组元素值,val为对应出现次数
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
//在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
//出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
for (Map.Entry<Integer, Integer> entry : map.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;
}
4.题目总结
解题思路里面