一.队列(Queue)的概念
概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)。入队列:进行插入操作的一端称为队尾(Tail/Rear)。出队列:进行删除操作的一端称为队头(Head/Front)。
![](https://img-blog.csdnimg.cn/img_convert/d4fa90baf4c4a4933322693427796271.png)
循环队列(OJ:设计循环队列)
实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列通常使用数组实现。
我们知道顺序表是具有随机访问的特点的 , 将若干个元素入队后 , 每次出队操作后 , 该元素原来所在的空间就无法再使用了 , 这就使得顺序表得空间利用不充分 ;
而如果采用循环队列就可以解决这个问题 , 如果数组最后一个空间已经有了元素 , 但前面由于出队有了空缺 , 此时再有元素入队就能重新从数组的尾部跳到数组的头部 , 对已经出队的空间进行重新利用 , 这样就避免了空间的浪费 。
![](https://img-blog.csdnimg.cn/img_convert/a47fac2cd4d96b07f259ee2b6362ee68.png)
数组下标循环的小技巧
1. 下标最后再往后(offset 小于 array.length): index = (index + offset) % array.length
![](https://img-blog.csdnimg.cn/img_convert/7267f30348ab972ee4cd55eceec85162.png)
2. 下标最前再往前(offset 小于 array.length): index = (index + array.length - offset) % array.length
![](https://img-blog.csdnimg.cn/img_convert/f93dfc95705a34eb32be117ad2598f47.png)
如何区分空与满
1. 通过添加 size 属性记录
2. 保留一个位置
3. 使用标记
![](https://img-blog.csdnimg.cn/img_convert/60454db6f0e7a102007c55f05cf4a524.png)
双端队列
双端队列(Deque)是指允许两端都可以进行入队和出队操作的队列,Deque 是 “double ended queue” 的简称。那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
双端队列同时遵守了先进先出和后进先出的原则,所以可以说它是一种把队列和栈相结合的一种数据结构 ;所以双端队列既能够当队列使用,也能当栈使用,Java底层是使用双链表(LinkedList)来实现双端队列(Deque)和队列(Queue)的 ;
限制一端进行出队或入队的双端队列称为受限的双端队列。
![](https://img-blog.csdnimg.cn/img_convert/fe5e437d847381adba291fa0bec7ed23.png)
二.队列的使用
集合-Queue,Deque类的介绍
在Java中,Queue和Deque是两个接口,底层是通过双链表实现的 , 使用时必须创建LinkedList的对象 。
这里观察Queue,Deque接口与LinkedList类关系;LinkedList类实现了Queue,Deque接口,Deque接口扩展了Queue接口,三者都扩展或继承了Collection和Iterable接口。
![](https://img-blog.csdnimg.cn/img_convert/ace2c59fb5f7316a5b22346e2b497588.png)
常用方法
由于在实际开发中Deque使用的并不是非常多 , 所以这里只列出Queue接口中常用的方法 :
![](https://img-blog.csdnimg.cn/img_convert/496cf034ec875370962de47164c2f108.png)
1. add系列方法与offer系列方法的区别:
两者都是在队列队头或队尾插入元素,前者(add)插入元素失败会引发异常,后者(offer)插入元素失败不会引发异常,只会以返回false的形式表示插入元素失败,如果是有容量限制的队列,使用offer系列方法更加合适。
2. remove系列方法与poll系列方法的区别:
两者都是在队列队头或队尾删除并返回元素,前者(remove)删除元素失败会引发异常,后者(poll)删除元素失败不会引发异常,只会以返回null的形式表示删除元素失败,如果是有容量限制的队列,使用poll系列方法更加合适。
3. get系列方法与peek系列方法的区别:
两者都是在队列队头或队尾获取并返回元素,前者(get)获取元素失败会引发异常,后者(peek)获取元素失败不会引发异常,只会以返回null的形式表示获取元素失败,如果是有容量限制的队列,使用peek系列方法更加合适。
代码实现
public static void main(String[] args) {
Queue<Integer> q = new LinkedList<>();
q.offer(1);
q.offer(2);
q.offer(3);
q.offer(4);
q.offer(5); // 从队尾入队列
System.out.println(q.size());
System.out.println(q.peek()); // 获取队头元素
q.poll();
System.out.println(q.poll()); // 从队头出队列,并将删除的元素返回
if(q.isEmpty()){
System.out.println("队列空");
}else{
System.out.println(q.size());
}
}
三.队列的模拟实现
思考
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过前面线性表的学习了解到常见的空间类型有两种:顺序结构 和 链式结构。那我们思考下:队列的实现使用顺序结构还是链式结构好?
在Java集合框架中的队列就是用双链表来实现的 , 在这里采用单链表来实现队列 .
我们这里设置单链表中的两个引用head和tail用来指向头节点和尾节点 , 需要注意的是我们应该让单链表的头做队头 , 单链表的尾做队尾 , 也就是从单链表的尾入队 , 头出队 , 此时入队和出队操作的时间时间复杂度都为O(1) ; 而如果反过来入队使用头插 , 入队的时间复杂度为O(1) , 此时出队要删除单链表最后一个节点 , 需要先找到其前一个节点 , 出队的时间复杂度就为O(N)了;
如果使用双链表来实现栈的话那就简单了 , 由于是是双向的 , 不管哪一端来做队头/队尾 , 时间复杂度都为O(1) ; 会了单链表的 , 双链表的不在话下了 .
代码实现
package demo;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
/**
* Created with IntelliJ IDEA.
* Description:
* User:YY
* Date:2023-02-16
* Time:15:46
*/
public class MyQueue {
static class ListNode {
private int val;
private ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
public ListNode tail;
public int usedSize;
public void offer (int val){
ListNode node = new ListNode(val);
if(this.head == null){
this.head = node;
this.tail = node;
}else {
this.tail.next = node;
this.tail = this.tail.next;
}
usedSize++;
}
public int poll() {
if(this.head == null){
return -1;
}
int ret = this.head.val;
this.head = this.head.next;
if(this.head == null){
this.tail = null;
}
usedSize--;
return ret;
}
public int peek(){
if(this.head == null){
return -1;
}
return this.head.val;
}
public boolean empty(){
return this.usedSize == 0;
}
public int getUsedSize(){
return this.usedSize;
}
public static void main(String[] args) {
MyQueue queue = new MyQueue();
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
System.out.println(queue.poll());
System.out.println(queue.peek());
System.out.println(queue.getUsedSize());
System.out.println(queue.empty());
System.out.println(queue.getUsedSize());
}
}