部分来源:Java数据结构中的栈和队列(带图解)_Owen_Xp的博客-优快云博客_java队列 堆栈
栈(stack)
什么是栈?
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。
压栈:向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;
出栈:从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈的使用
栈的工作原理:后进先出
操作:new(新建)push(入栈)pop(出栈)peek(获取栈顶元素)isEmpty(判断栈是否为空)
根据这些操作方法来使用栈
方法 | 功能 |
---|---|
Stack() | 构造一个空的栈 |
push(E e) | 将e入栈,并返回e |
pop() | 将栈顶元素出栈并返回,并删除它(出栈) |
peek() | 获取栈顶元素,但是并不在堆栈中删除它(不出栈) |
int size() | 获取栈中有效元素个数 |
boolean empty() | 检测栈是否为空 |
栈的模拟实现
从上图可以看出,Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的。
队列(Queue)
什么是队列?
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
队列的使用
在Java中,Queue是个接口,底层是通过链表实现的。
方法 | 功能 |
add() | 从队尾压入元素,超出容量时,add()方法会对抛出异常 |
offer() | 从队尾压入元素,超出容量时,offer()方法会返回false |
poll()、remove() | 容量大于0的时候,删除并返回队头元素;在容量为0的时候,remove()会抛出异常,poll()返回false |
peek()、element() | 容量大于0的时候,都返回队头元素,但是不删除;容量为0的时候,element()会抛出异常,peek()返回null |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
注意: Queue 是个接口,在实例化时必须实例化 LinkedList 的对象,因为 LinkedList 实现了 Queue 接口。
队列的分类
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过线性表的学习了解到常见的空间类型有两种:顺序结构和链式结构 。
一、顺序队列
顺序队列是队列的一种,只是说队列的存在形式是队列中的元素是连续的,就像数组
(1)队头不动,队尾动
在元素出队列时,队头不动,队尾动,每出一个元素,就把队列中剩余的元素往前搬移,从而队尾会跟着改变。
缺点:在出队列时,需要大量搬移元素,时间复杂度为O(N)
(2)队头动,队尾不动
在元素出队列时,队头往后移动一个位置,队尾一直保持不动。
优点:在出队列时,只需把队头往后移动一个位置就好,所以时间复杂度为O(1)。
缺点:因为在元素在出队列时只是移动队头,并没有把元素在内存上删除,所以就会出现元素把整个内存耗尽,并且此内存中的存储都是不合法的元素,从而造成假溢出。
假溢出:看似内存中再存储不了元素了,但是此内存在理论上是可以使用的,内存被一些不合法的元素使用完了,从而造成一种溢出现象,但是此溢出是假溢出。
循环队列
什么是循环队列?
循环队列是首尾相接的顺序存储队列,顾名思义循环队列就是队列的内存可以循环使用
在顺序队列中,出现了假溢出问题,那循环队列就是为了假溢出问题的,假溢出造成了内存浪费,循环队列可以使得内存得到有效的利用
给定两个指针,分别标记队头和队尾,标记队头的指针是front,标记队尾的是rear
1、空队列
开始时,front是队头指针,rear是队尾指针,空队列时front和rear指向同一位置
2、队列中有元素
在插入元素时,rear会随着元素的插入而向后移动,front不会动,如果删除元素的话,那rear会向前移动,front也还是不会动。
循环队列如何进行判断是否存满?或者为空呢?
1、少使用一个循环队列中的存储空间
在存储元素时,少用一个存储单元,那队列空的时候,front和rear指向同一位置,那随着插入元素,rear向后移动,最后一个存储单元不使用,所以队列满的时候,rear的下一个位置就是front指向的元素
队列为空时的判断条件:front==rear时 ,队列为空队列
队列为满时的判断条件:(rear+1)%M == front时,队列为满队列
ps:M是循环队列的空间大小,即就是M标记了循环队列中最多可以存储几个元素
本来rear+1就是front指向的位置,那为什么还要跟队列的空间大小取模呢?是因为当rear指向最后一个位置的时候,下标为7,那rear+1=8,而循环队列中没有下标为8的元素,如图可知,必须对相加的结果取模
2、给一个标记flag,默认flag为false
给一个标记flag ,默认此标记为false,当插入元素就把标记flag改为true,当删除元素就把标记改为false
ps:当队列为空的时候,front和rear指向同一位置,当队列为满的时候,front和rear也是指向同一位置
队列为空时的判断条件:front==rear&&flag==false
队列为满时的判断条件:front==rear&&flag==true
3、给一个计数器count
插入一个元素时count++,删除一个元素时count - -
队列为空时的判断条件:count==0
队列为满时的判断条件:count=M 或者(count>0 && rear==front)
综上述三种方法,第三种方法是最简单方便的
二、链式队列
链式队列是队列的一种,与顺序队列的存储方式不一样,跟线性表的单链表一样,只是说只能从头出从尾进而已
/*
* 链式队列
* front:指向的是链表的头节点
* rear:永远指向的是末尾节点
* @param <T>
*/
class LinkQueue<T>{
// 构造函数 offer poll peek empty size
private Entry<T> front; // 队头
private Entry<T> rear;
private int count; // 记录链表节点的个数
public LinkQueue(){
front= new Entry<>(null, null);
}
public void offer(T val) {
Entry<T> node = front;
while(node.next != null){
node = node.next;
}
node.next = new Entry<>(val, null);
count++;
}
public T poll() {
if(empty())
return null;
T val = front.next.data;
front.next = front.next.next;
count--;
return val;
}
public T peek() {
if(empty())
return null;
return front.next.data;
}
public boolean empty(){
return front.next == null;
}
public int size(){
return this.count;
}
static class Entry<T>{
T data;
Entry<T> next;
public Entry(T data, Entry<T> next) {
this.data = data;
this.next = next;
}
}
}
public class LinkQueueTest {
public static void main(String[] args) {
LinkQueue<Integer> ls = new LinkQueue<>();
ls.offer(1);
ls.offer(2);
ls.offer(3);
ls.offer(4);
System.out.println(ls.peek());
System.out.println(ls.size());
while(!ls.empty()){
System.out.print(ls.poll() + " ");
}
System.out.println();
}
}
双端队列(Deque)
双端队列又名double ended queue
,简称deque,双端队列没有队列和栈这样的限制级,它允许两端进行入队和出队操作,也就是说元素可以从队头出队和入队,也可以从队尾出队和入队。
import java.util.List;
// 模拟实现队列---底层使用双向链表---在集合框架中Queue是一个接口---底层使用的是LinkedList
public class Queue<E> {
// 双向链表节点
public static class ListNode<E>{
ListNode<E> next;
ListNode<E> prev;
E value;
ListNode(E value){
this.value = value;
}
}
ListNode<E> first; // 队头
ListNode<E> last; // 队尾
int size = 0;
// 入队列---向双向链表位置插入新节点
public void offer(E e){
ListNode<E> newNode = new ListNode<>(e);
if(first == null){
first = newNode;
// last = newNode;
}else{
last.next = newNode;
newNode.prev = last;
// last = newNode;
}
last = newNode;
size++;
}
// 出队列---将双向链表第一个节点删除掉
public E poll(){
// 1. 队列为空
// 2. 队列中只有一个元素----链表中只有一个节点---直接删除
// 3. 队列中有多个元素---链表中有多个节点----将第一个节点删除
E value = null;
if(first == null){
return null;
}else if(first == last){
last = null;
first = null;
}else{
value = first.value;
first = first.next;
first.prev.next = null;
first.prev = null;
}
--size;
return value;
}
// 获取队头元素---获取链表中第一个节点的值域
public E peek(){
if(first == null){
return null;
}
return first.value;
}
public int size() {
return size;
}
public boolean isEmpty(){
return first == null;
Java中Queue和Deque
1.Deque是Queue的子接口;
从源码中可以得知:Queue以及Deque都是继承于Collection,Deque是Queue的子接口。
public interface Deque<E> extends Queue<E> {}
2.Queue——单端队列;Deque——双端队列;
从Deque的解释中,我们可以得知:Deque是double ended queue,我将其理解成双端队列,就是可以在首和尾都进行插入或删除元素。
而Queue的解释中,Queue就是简单的FIFO(先进先出)队列。所以在概念上来说,Queue是FIFO的单端队列,Deque是双端队列。
3.Queue常用子类——PriorityQueue;Deque常用子类——LinkedList以及ArrayDeque;
Queue有一个直接子类PriorityQueue。
而Deque中直接子类有两个:LinkedList以及ArrayDeque。
PriorityQueue:
从源码中,明显看到PriorityQueue的底层数据结构是数组,而无边界的形容,那么指明了PriorityQueue是自带扩容机制的,具体请看PriorityQueue的grow方法。
LinkedList以及ArrayDeque:
从官方解释来看,ArrayDeque是无初始容量的双端队列,LinkedList则是双向链表。
而我们还能看到,ArrayDeque作为队列时的效率比LinkedList要高。
而在栈的使用场景下,无疑具有尾结点,不需判空的LinkedList更为高效。
其实ArrayDeque和LinkedList都可以作为栈以及队列使用,但是从执行效率来说,ArrayDeque作为队列,以及LinkedList作为栈使用,会是更好的选择。
- PriorityQueue可以作为堆使用,而且可以根据传入的Comparator实现大小的调整,会是一个很好的选择。
- ArrayDeque可以作为栈或队列使用,但是栈的效率不如LinkedList高,通常作为队列使用。
- LinkedList可以作为栈或队列使用,但是队列的效率不如ArrayQueue高,通常作为栈使用。
4.Queue和Deque的方法区别:
在java中,Queue被定义成单端队列使用,Deque被定义成双端队列使用。
而由于双端队列的定义,Deque可以作为栈或者队列使用;
而Queue只能作为队列或者依赖于子类的实现作为堆使用。
方法上的区别如下:
Queue | Deque |
---|---|
add(将指定元素插入此队列尾部,成功返回true;当超出容量时add()会抛出异常 ) | addLast、offerLast(在队列尾部插入元素) |
offer(将指定元素插入此队列尾部,成功返回true;当超出容量时offer()会返回false) | pollFirst、removeFirst、getFirst、peekFirst(获取头部元素) |
remove(获取并移除队列的头部元素,队列为空抛出异常) | getLast、peekLast(获取但不移除队列最后一个元素) |
poll(获取并移除队列的头部元素,队列为空返回null) | pollLast、removeLast(获取并移除双端队列最后一个元素) |
element(获取但是不移除队列头部元素,队列为空抛出异常) | offerFirst(将指定元素插入队列开头) remove(Object o) 从双端队列中移除第一次出现的指定元素o |
peek(获取但是不移除队列头部元素,队列为空返回null) | pop(从双端队列表示的堆栈中弹出一个元素) push(将一个元素推入双端队列表示的堆栈,即队列的头部。成功返回true,如果没有可用空间,抛出异常) |
isEmpty(判断队列是否为空,为空返回true) | isEmpty(判断队列是否为空,为空返回true) |
size(获取队列元素数量) | size(返回双端队列元素数) |
Deque中红色标记方法均和Queue中方法一一对应;且queue中的方法,deque中均可用。
Deque接口扩展(继承)了 Queue 接口。在将双端队列用作队列时,将得到 FIFO(先进先出)行为。将元素添加到双端队列的末尾,从双端队列的开头移除元素。从 Queue 接口继承的方法完全等效于 Deque 方法,如下表所示:
Queue方法 | 等效Deque方法 |
add(e) | addLast(e) |
offer(e) | offerLast(e) |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
双端队列也可用作 LIFO(后进先出)堆栈。应优先使用此接口而不是遗留 Stack 类。在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈方法完全等效于 Deque 方法,如下表所示:
堆栈方法 | 等效Deque方法 |
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
用栈实现队列
使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。
下面动画模拟以下队列的执行过程如下:
执行语句:
queue.push(1);
queue.push(2);
queue.pop(); 注意此时的输出栈的操作
queue.push(3);
queue.push(4);
queue.pop();
queue.pop();注意此时的输出栈的操作
queue.pop();
queue.empty();
在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
在代码实现的时候,会发现pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以思考一下如何把代码抽象一下。
class MyQueue {
Stack<Integer> stackIn;
Stack<Integer> stackOut;
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
public void push(int x) {
stackIn.push(x);
}
public int pop() {
dumpstackIn();
return stackOut.pop();
}
public int peek() {
dumpstackIn();
return stackOut.peek();
}
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
//复用方法:如果stackOut为空,那么将stackIn中的元素全部放到stackOut中
private void dumpstackIn(){
if(!stackOut.isEmpty()){
return;
}
while(!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
用队列实现栈
刚刚做过用栈实现队列的同学可能依然想着用一个输入队列,一个输出队列,就可以模拟栈的功能,仔细想一下还真不行!
队列模拟栈,其实一个队列就够了,那么我们先说一说两个队列来实现栈的思路。
队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。
所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。
但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用又来备份的!
如下面动画所示,用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
模拟的队列执行语句如下:
queue.push(1);
queue.push(2);
queue.pop(); // 注意弹出的操作
queue.push(3);
queue.push(4);
queue.pop(); // 注意弹出的操作
queue.pop();
queue.pop();
queue.empty();
用两个单端队列(两个 Queue)实现方法:
class MyStack {
Queue<Integer> queue1;// 和栈中保持一样元素的队列
Queue<Integer> queue2;// 辅助队列
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
public void push(int x) {
queue2.offer(x);
while(!queue1.isEmpty()){
queue2.offer(queue1.poll());
}
queue<Integer> queueTemp; //将queue1中的元素赋值到queue2中(为了形成先入后出的情形)
queueTemp = queue1;
queue1 = queue2;
queue2 = queueTemp;
}
public int pop() {// 因为queue1中的元素已经和栈中的保持一致,所以这个和下面两个的操作只看queue1即可
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
用两个双端队列(两个 Deque)实现方法:
class MyStack {
// Deque接口继承了Queue 接口
// 所以Queue中的 add、poll、peek等效于Deque中的 addLast、pollFirst、peekFirst
Deque<Integer> que1; // 和栈中保持一样元素的队列
Deque<Integer> que2; // 辅助队列
public MyStack() {
que1 = new ArrayDeque<>();
que2 = new ArrayDeque<>();
}
public void push(int x) {
que1.addLast(x);//双端队列,将元素插入队列尾部
}
public int pop() {
int size = queq.size();
size--;
while(size-- > 0){
que2.addLast(que1.peekFirst);//获取que1中的元素并加入que2中
que1.pollFirst();//que1中删除获取的队列顶部元素
}
int res = que1.pollFirst();//res获取que1中删除的留下的最后一个元素
// 将 que2 对象的引用赋给了 que1 ,此时 que1,que2 指向同一个队列
que1 = que2;//获取元素后,再将que2中的元素赋值给que1
// 如果直接操作 que2,que1 也会受到影响,所以为 que2 分配一个新的空间
que2 = new ArrayDeque<>();
return res;
}
public int top() {
return que1.peekLast(); //双端队列,获取队列中顶部元素
}
public boolean empty() {
return que1.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
用一个队列实现方法:(一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了)
class MyStack {
Deque<Integer> que1;
public MyStack() {
que1 = new ArrayDeque<>();
}
public void push(int x) {
que1.addLast(x);
}
public int pop() {
int size = que1.size();
size--;
while(size-- > 0){
que1.addLast(que1.peekFirst());
que1.pollFirst();
}
int res = que1.pollFirst();
return res;
}
public int top() {
return que1.peekLast();
}
public boolean empty() {
return que1.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
相关题目
一、有效的括号
先来分析一下 这里有三种不匹配的情况,
- 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
- 第二种情况,括号没有多余,但是 括号的类型没有匹配上。
- 第三种情况,字符串里右方向的括号多余了,所以不匹配。
我们的代码只要覆盖了这三种不匹配的情况,就不会出问题,可以看出 动手之前分析好题目的重要性。
动画如下:
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。
分析完之后,代码其实就比较好写了,
但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
class Solution {
public boolean isValid(String s) {
Deque<Character> stack = new LinkedList<>();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(c == '('){
stack.push(')');
}else if(c == '['){
stack.push(']');
}else if(c == '{'){
stack.push('}');
// 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
// 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
}else if(stack.isEmpty() || stack.peek() != c){
return false;
}else{ //和栈顶元素匹配的右括号,删除
stack.pop();
}
}
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return stack.isEmpty();
/**
//左边括号先入栈的方法
int n = s.length();
if(n % 2 == 1) return false;
Map<Character, Character> map = new HashMap<>(){{
put(')', '(');
put(']', '[');
put('}', '{');
}};
Deque<Character> stack = new LinkedList<>();
for(int i = 0; i < n; i++){
char c = s.charAt(i);
if(map.containsKey(c)){
if(stack.isEmpty() || stack.peek() != map.get(c)){
return false;
}else{
stack.pop();
}
}else{
stack.push(c);
}
}
return stack.isEmpty();
*/
}
}
二、删除字符串中所有相邻重复项
本题要删除相邻相同元素,其实也是匹配问题,相同左元素相当于左括号,相同右元素就是相当于右括号,匹配上了就删除。
那么再来看一下本题:可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。
如动画所示:
从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以在对字符串进行反转一下,就得到了最终的结果。
class Solution {
public String removeDuplicates(String s) {
Deque<Character> stack = new LinkedList<>();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(stack.isEmpty() || stack.peek() != c){
stack.push(c);
}else{
stack.pop();
}
}
String str = "";
while(!stack.isEmpty()){
str = stack.pop() + str;
}
return str;
}
}
字符串类本身就提供了类似「入栈」和「出栈」的接口,因此我们直接将需要被返回的字符串作为栈即可。
class Solution {
public String removeDuplicates(String s) {
StringBuilder res = new StringBuilder();
int top = -1; //top表示res的长度
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
// 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
if(top >= 0 && res.charAt(top) == c){
res.deleteCharAt(top);
top--;
// 否则,将该字符 入栈,同时top++
}else{
res.append(c);
top++;
}
}
return res.toString();
}
}
三、逆波兰表达式求值
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList<>();
for(String c : tokens){
if("+".equals(c)){ // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
stack.push(stack.pop() + stack.pop());
}else if("-".equals(c)){
stack.push(-stack.pop() + stack.pop());// 注意 - 和/ 需要特殊处理(所有运算都是后一个数对前一个数)
}else if("*".equals(c)){
stack.push(stack.pop() * stack.pop());
}else if("/".equals(c)){
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 / temp1);
}else{
stack.push(Integer.valueOf(c));
}
}
return stack.pop();
/**
Deque<Integer> stack = new LinkedList<>();
for(int i = 0; i < tokens.length; i++){
if("+".equals(tokens[i]) || "-".equals(tokens[i]) || "*".equals(tokens[i]) || "/".equals(tokens[i])){
int num1 = stack.pop();
int num2 = stack.pop();
if("+".equals(tokens[i])) stack.push(num2 + num1);
if("-".equals(tokens[i])) stack.push(num2 - num1);
if("*".equals(tokens[i])) stack.push(num2 * num1);
if("/".equals(tokens[i])) stack.push(num2 / num1);
}else{
stack.push(Integer.valueOf(tokens[i]));
}
}
return stack.pop();
*/
}
}
四、滑动窗口最大值(单调队列)
思路
这是使用单调队列的经典题目。
暴力方法,遍历一遍的过程中每次从窗口中在找到最大的数值,这样很明显是$O(n × k)$的算法。
有的同学可能会想用一个大顶堆(优先级队列)来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了, 但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。
此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
这个队列应该长这个样子:
class MyQueue {
public:
void pop(int value) {
}
void push(int value) {
}
int front() {
return que.front();
}
};
每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。
这么个队列香不香,要是有现成的这种数据结构是不是更香了!
可惜了,没有! 我们需要自己实现这么个队列。
然后在分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列(单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列)。没有直接支持单调队列,需要我们自己来一个单调队列
不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。
来看一下单调队列如何维护队列里的元素。
动画如下:
对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。
此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口经行滑动呢?
设计单调队列的时候,pop,和push操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下:
那么我们用什么数据结构来实现这个单调队列呢?
使用deque最为合适,可以在头尾入队和出队元素。
class Solution{
public int[] maxSlidingWindow(int[] nums, int k) {
/**
队列表示滑动窗口:头尾尾头口诀(i为当前即将入队元素下标;k为滑动窗口大小)
1.头:清理超期元素(清理i-k位置的元素)
2.尾:维护单调递减队列(清除队列内<新入队的元素)
3.尾:新元素入队
4.头:获取滑动窗口最大值(返回头部元素,i>=k-1时)
*/
Deque<Integer> queue = new LinkedList<>(); //存放数组元素下标
int[] res = new int[nums.length - k + 1];
int resIndex = 0;
for(int i = 0; i < nums.length; i++){
//1.头:清理超期元素(清理i-k位置的元素)每次滑动窗口只能有3个值
if(!queue.isEmpty() && queue.peek() == i - k){
queue.poll();
}
//2.尾:维护单调递减队列(清除队列内<新入队的元素)
while(!queue.isEmpty() && nums[i] > nums[queue.peekLast()]){
queue.removeLast();
}
//3.尾:新元素入队
queue.add(i);
//4.头:获取滑动窗口最大值(返回头部元素,i>=k-1时)
if(i >= k - 1){ //因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
res[resIndex++] = nums[queue.peek()];
}
}
return res;
}
}