Java队列(从内容结构到经典练习一步到位)
提示:宝藏博主,赶紧码住(づ ̄3 ̄)づ╭❤~
Java队列
- 1.队列
- 2. 核心接口
- 3. 模拟实现
- 3.1 链式实现
- 3.2 线性实现
- 4. 子接口
- 5. 常见非并发实现类
- 5.1 LinkedLsit
- 5.2 ArrayDeque
- 5.3 PriorityQueue
- 6. 典型题目讲解
- 6.1 用队列实现栈
- 6.2 用栈实现队列
1.队列
队列是一种非常重要的数据结构,遵循先进先出原则.只允许在一端进行插入,另一端进行删除,插入的一端称为队尾,删除的一端是对头.
生活中排队买东西就是这个理,先到的先服务,先离开:
队列的两个基本操作是:
- 入队(Enqueue):将元素添加到队列的尾部
- 出队(Dequeue):从队列的头部移除并返回元素
2. 核心接口
Java中的队列主要通过java.util.Queue
接口来实现,该接口扩展了java.util.Collection
接口,并添加了一些方法,通过源码查看:
即:
操作类型 | 方法 | 描述 | 失败时的行为 |
---|---|---|---|
插入 | boolean offer(E e) | 将元素插入队列尾部 | 若队列已满,返回false |
boolean add(E e) | 若队列已满,抛出异常 | ||
移除 | E poll() | 移除对头元素并返回 | 若队列为空,返回false |
E remove() | 若队列为空,抛出异常 | ||
查看 | E peek() | 获取对头元素并返回 | 若队列为空,返回false |
E element() | 若队列为空,抛出异常 |
通过上述梳理很容易看出,上述6个方法其实只有3种不同作用,可以分为两组:
offer()
,poll()
,peek()
是一组,通过返回值来指示操作成功与否,而不是异常,代码不易中断add()
,remove()
,element()
是一组,通过抛出异常来说明操作是否成功
offer(),poll(),peek()用得更多~
3. 模拟实现
主要针对offer(),poll(),peek(),isEmpty()方法
3.1 链式实现
接下来,我们使用双向链表模拟实现一下:
public class MyQueue {//内部节点类static class ListNode {public int val;public ListNode prev;public ListNode next;public ListNode(int val) {this.val = val;}}public ListNode head;public ListNode tail;//尾插法public void offer(int val) {ListNode node=new ListNode(val);if(head==null) {head=tail=node;}else {tail.next=node;node.prev=tail;tail=node;}}//头删public int poll() {if(head==null) return -1;//当然这里抛出一个异常更合适(我偷懒了),因为head的值也有可能为-1int ret=head.val;if(head.next==null) {head=null;tail=null;}else {head=head.next;head.prev=null;}return ret;}//获取队首元素public int peek() {if(head==null) return -1;//当然这里抛出一个异常更合适(我偷懒了),因为head的值也有可能为-1return head.val;}//判断队列是否为空public boolean isEmpty() {return head==null;}}
3.2 线性实现
上面3.1是用LinkedList取实现的队列,用数组也能实现,并且这个数组还有一个专门的名字,叫:循环队列
循环队列(Circular Queue)是一种固定大小数组实现的队列.
当你试图用普通数组实现一个队列时:
如图,定义两个指针first,last分别指向头和尾,当不断进行尾插头删操作后,first和last都不断向后移动,first前面会出现一片空闲位置,但是此时last已经走到了数组末端,无法添加新元素了,这种现象也叫"假溢出".
循环队列的出现解决了"假溢出"的问题:逻辑上将数组的首尾相连形成一个环,当指针到达数组末尾时,可以重新回到开头使用刚才抛出元素留下的空间.
但是似乎还是存在问题:如何判断循环队列是满的还是空的?
为什么这么问,因为在一开始,队列中没有元素,first和last指向同一个位置;而当不断尾插元素,last一度超过了first,然后将越来越靠近first,最后两者会相遇,仍然指向同一位置,如果数组把这次的相遇和一开始当做同样的情况处理,可不就出问题了.
对此,我们提出来一个解决方案:牺牲一个空间,这个空间不存储元素,而是作为数组将满的预警:
这个空间是动态的,隐藏在last的下一个位置,当last的下一个位置就是first时,就意味着数组满了.
还有一个问题,如何处理fisrt和last的遍历条件?出队时first++,入队时last++吗?
但是一直++越界了咋整?
对此,伟大的计算机科学家们想到了取模运算:
first=(first+1)%array.length;
last=(last+1)%array.length;
如上图的情况,计算last的下一个位置,使用last++时便会造成数组越界,而使用(7+1)%8=0,便可让last回到原数组的开头,进而实现循环遍历的效果.
接下来,通过一道编程题将理论转化为代码:
622. 设计循环队列 - 力扣(LeetCode)
class MyCircularQueue {public int[] elem;public int first;public int last;public MyCircularQueue(int k) {this.elem=new int[k+1];}//入队public boolean enQueue(int value) {//先判断满了没有if(isFull()) return false;//满了,入队失败elem[last]=value;last=(last+1)%elem.length;return true;}//出队public boolean deQueue() {//先判断是否为空if(isEmpty()) return false;//队列为空,出队失败first=(first+1)%elem.length;return true;}public int Front() {if(isEmpty()) return -1;return elem[first];}public int Rear() {if(isEmpty()) return -1;//返回last的前一个元素,要对last=0的情况进行特别处理int index=(last==0)?elem.length-1:last-1;return elem[index];}public boolean isEmpty() {return last==first;}public boolean isFull() {return (last+1)%elem.length==first;//判断last逻辑上的下一个元素与first的关系}
}
4. 子接口
双端队列Deque(Double Ended Queue)扩展了Queue,允许在头部和尾部进行插入和删除操作,这意味着它可以作为:
- 队列:使用
offerLast()
(尾删)和pollFirst()
(头插) - 栈:使用
push()/addFirst
和pop()/removeFirst()
- 双端队列:在两头进行操作
5. 常见非并发实现类
从上述Java集合框架示意图中可以清晰看到,LinkedList
,ArrayDeque
,PriorityQueue
这三个类都实现了Queue接口,其中LinkedList,ArrayDeque还实现了其子接口Deque.
5.1 LinkedLsit
LinkedList是用链表实现的队列,插入和删除效率高.
import java.util.LinkedList;
import java.util.Queue;public class LinkedListQueueDemo {public static void main(String[] args) {Queue<String> queue = new LinkedList<>();// 入队queue.offer("A");queue.offer("B");queue.offer("C");// 查看队首元素(不移除)System.out.println("队首元素: " + queue.peek()); // A// 出队System.out.println("出队: " + queue.poll()); // ASystem.out.println("出队: " + queue.poll()); // BSystem.out.println("出队: " + queue.poll()); // CSystem.out.println("出队: " + queue.poll()); // null(队空)}
}
5.2 ArrayDeque
ArrayDeque基于动态数组实现,通常比LinkedList更快,因为内存连续,缓存友好.
import java.util.ArrayDeque;
import java.util.Queue;public class ArrayDequeQueueDemo {public static void main(String[] args) {Queue<String> queue = new ArrayDeque<>();// 入队queue.offer("A");queue.offer("B");queue.offer("C");// 查看队首元素System.out.println("队首元素: " + queue.peek()); // A// 出队System.out.println("出队: " + queue.poll()); // ASystem.out.println("出队: " + queue.poll()); // BSystem.out.println("出队: " + queue.poll()); // C}
}
5.3 PriorityQueue
PriorityQueue可以按照元素的优先级实现队列.
import java.util.PriorityQueue;
import java.util.Queue;public class PriorityQueueDemo {public static void main(String[] args) {Queue<Integer> queue = new PriorityQueue<>();// 入队(无序插入)queue.offer(30);queue.offer(10);queue.offer(20);// 注意:PriorityQueue 是按优先级排序的(默认自然顺序:小的优先)System.out.println("队首元素: " + queue.peek()); // 10// 出队(自动按优先级取出最小值)System.out.println("出队: " + queue.poll()); // 10System.out.println("出队: " + queue.poll()); // 20System.out.println("出队: " + queue.poll()); // 30}
}
6. 典型题目讲解
6.1 用队列实现栈
225. 用队列实现栈 - 力扣(LeetCode)
用一个队列能实现栈吗?不能,两者的原理在某种程度来说是相反的,一个先进先出,而另一个确实先进后出.
因此需要准备两个队列:
要取出栈顶元素34,但是它却是队列中最后一个弹出的,因此需要从第一个队列queue1中出队n-1个元素到第二个空的队列queue2中,最后queue1剩下的那个就是要弹出的栈顶元素.
peek操作也是这么个理,稍微有一点不同:将queue1的元素全部弹出给queue2,在此过程中,定义一个变量tmp存储一下刚才弹出的,当queue1空了的时候,tmp就是要peek的元素了.
后面再push时,我们只需要将元素放入不为空的那个队列即可,若是第一次放呢?那就规定一下,放进第一个队列queue1中.
理论成立,开始coding
class MyStack {public Queue<Integer> queue1;public Queue<Integer> queue2;public MyStack() {queue1=new LinkedList<>();queue2=new LinkedList<>();}public void push(int x) {if(empty()) {//两队列都为空时默认放入queue1中queue1.offer(x);return;} if(!queue1.isEmpty()) { //不是都为空的话,往不空的那个放queue1.offer(x);}else {queue2.offer(x);}}public int pop() {if(empty()) return -1;if(!queue1.isEmpty()) {int size=queue1.size();//注意队列的size是会变的,这里要定义一个变量将其存储起来for(int i=0;i<size-1;i++) {queue2.offer(queue1.poll());}return queue1.poll();}else {int size=queue2.size();//注意队列的size是会变的,这里要定义一个变量将其存储起来for(int i=0;i<size-1;i++) {queue1.offer(queue2.poll());}return queue2.poll();}}public int top() {if(empty()) return -1;if(!queue1.isEmpty()) {int ret=0;int size=queue1.size();//注意队列的size是会变的,这里要定义一个变量将其存储起来for(int i=0;i<size;i++) {ret=queue1.poll();queue2.offer(ret);}return ret;}else {int ret=0;int size=queue2.size();//注意队列的size是会变的,这里要定义一个变量将其存储起来for(int i=0;i<size;i++) {ret=queue2.poll();queue1.offer(ret);}return ret;}}public boolean empty() {return queue1.isEmpty() && queue2.isEmpty();//当两个队列都为空时才返回true}
}
6.2 用栈实现队列
232. 用栈实现队列 - 力扣(LeetCode)
首先,还是那个问题,一个栈能实现队列吗?
也还是那个回答,不能.
并且思路也和上道题不一样,stack同一端进出,从stack1弹出到stack2,则两个栈中的元素顺序就完全颠了个个,队列则不一样,从一个队列弹出元素到另一个队列或者栈,两者的元素顺序是完全相同的.
如图,queue和stack1中元素压入顺序是完全相同的,弹出顺序则是完全相反的,那么要想得到完全一样的弹出顺序,只消把stack1中的元素全部压入stack2中,使得两栈中元素弹出顺序完全相反,那么queue和stack2的弹出顺序就完全相同了.
后面要是有元素offer进来,就固定放入stack1就好,等到stack2中元素弹出完了以后,再把stack1中的元素全部放进stack2.这样分工明确,stack1只管压入元素,stack2只管弹出栈顶元素.
class MyQueue {Stack<Integer> stack1;Stack<Integer> stack2;public MyQueue() {stack1=new Stack<>();stack2=new Stack<>();}public void push(int x) {stack1.push(x);}public int pop() {if(empty()) return -1;if(stack2.empty()) {while(!stack1.empty()) {stack2.push(stack1.pop());}}return stack2.pop();}public int peek() {if(empty()) return -1;if(stack2.empty()) {while(!stack1.empty()) {stack2.push(stack1.pop());}}return stack2.peek();}public boolean empty() {return stack1.isEmpty() && stack2.isEmpty();}
}