最近博主在学习JavaWeb的过程中,讲到了具体线程的知识,在写生产与消费者模型的具体代码时,发现涉及到了循环队列的知识,于是打算再次复习一下循环队列的具体编写
我们先复习一下队列的相关知识
只允许在一端进行插入数据操作,在另一端进行删除操作的特殊线性表,队列具有先进先出的特点
进行插入操作的一端称为队尾(rear)
进行删除操作的一端称为队头(front)
我们来看一下Java集合当中的有关队列的相关接口和类
我们可以看到 Queue 队列这个接口 底层可以是链表或者 顺序表来实现的 ,而在Java当中队列使用双端队列来进行维护的,同时 Deque 双端队列 也继承了Queue 这个接口
我们要想实例化具体队列的对象,必须new 一个 LinkedList 这个类出来。
Queue<Integer> queue = new LinkedList<>();
Deque<Integer> deque = new LinkedList<>();
LinkedList<Integer> queue2 = new LinkedList<>();
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。
那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
Deque 我们一看就知道Deque双端队列的方法比 Queue的方法多,因为双端队列可以可以从队头出(pollFirst())、队尾出(pollLast()),还可以从队头入(offerFirst())、队尾入(offerLast())
大家光看可能不太熟悉,我带着大家一块写代码来使用队列的一些常见方法
创建一个实例化对象
Queue<Integer> queue = new LinkedList<>();
入队操作
Queue 这个接口中给我们提供了 offer() 这个方法来进行入队操作
给这个队列进行入队操作,添加一些元素(入队操作使用的是尾插法)
queue.offer(1);
queue.offer(2);
queue.offer(3);
queue.offer(4);
现在队头元素是1,我们使用peek方法查看一下队首元素
出队操作
Queue 这个接口给我们提供了 poll()方法,返回队首元素并删除
也提供了 peek()方法,返回队首元素不删除
queue.poll();
queue.poll();
我们将之前入队的元素进行出队两次,现在队首元素是3
查看一下
判断是否为空
Queue 提供了 isEmpty() 判断队列是否为空,返回值是 true 或 false
// 在进行两次出队操作,此时队列应该为空
queue.poll();
queue.poll();
System.out.println(queue.isEmpty());
运行结果
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
在这里我们用 链表来实现 队列的内部常见方法
// 用单链表来实现简单的队列内部方法
// 先建立一个Node类,队列中的每个元素都相当于一个节点
class Node{
public int val;
public Node next;
// 写一个这个类的构造方法
public Node(int val) {
this.val = val;
}
}
public class MyQueue {
// 我们想呀,一个队列肯定是有队头,也肯定有一个队尾的
// 所以定义 队头和队尾
private Node first ;
private Node last;
// 入队
// 入队这里要用到尾插法
public void offer(int val){
Node cur = new Node(val);
// 第一次插入时,我们要把头尾指针指向这个节点
if(this.first == null){
this.first = cur;
this.last = cur;
}else{
// 不是第一次插入,那么就用尾插法的思想进行插入元素
this.last.next =cur;
this.last = cur;
}
}
// 出队
public int poll(){
if(isEmpty()){
throw new UnsupportedOperationException("队列为空");
}
int ret = this.first.val;
this.first = this.first.next;
return ret;
}
// 得到队头元素但不删除
public int peek(){
if(isEmpty()){
throw new UnsupportedOperationException("队列为空");
}
int ret = this.first.val;
return ret;
}
// 队列是否为空
public boolean isEmpty(){
if(this.first== null){
return true;
}
return false;
}
}
实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。
环形队列通常使用数组实现
我们就借助一道题来开启今天的设计循环队列吧…
这道题让我们来设计一个循环队列,并且把相关方法给实现
我们先来简单了解一下循环队列的结构
循环队列的底层其实是一个数组,为什么是循环呢?
就是我们来看
在这样的一个数组中,如果我们每一个格子都放满了,将队首元素出队,还想入一个元素,那么就从数组的0下标继续存储
这样的数组模型可能不太好理解,我们可以把这个数组看成环形的数组空间
front 是队首元素的下标
rear 是当前可存放元素的下标
当然我们能够想得到 入队出队的方式:
假如要入一个元素x,x 存放到 elem[rear],rear++;
假如要出一个元素x,直接 front++;
我们在这个环形的空间内不断入队出队,可以实现循环的效果
了解了循环队列的结构,那么随之而来的问题就很多了
第一个问题:front 和 rear 相遇之后到底是满还是空呢?
上图有两种情况,
第一种:当队列存满之后,front和rear就会相遇
第二种,当队列为空时,front 和rear 也会相遇
第二个问题:rear 每次存放完成之后,能不能进行 rear= rear+1?
在这种情况下,我们想想继续存入元素,那么 rear 如何从下标7 到下标1呢?
第三个问题: 每次出队操作时,能不能进行 front = front+1?
这种情况下,我们还想继续出队,可以 front = front+1 吗?
我们接下来逐一解决这些问题
首先第一个问题,
rear 是代表当前可以存放数组元素的下标
因为 front 、rear 相遇无法判断满还是空,所以我们干脆浪费一个数据元素的空间,最后一个空间不放元素,帮我们来判断是空还是满
我们现在想把 89 放到这个队列当中,但是每次放入元素时都要判断队列是否已经满了,那么此时我们看一下 rear下标的下一个 是不是 front,如果是的话就说明满了。
所以解决第一个问题是怎样解决的呢?
每次在放元素的时候都去判断当前rear 的下一个是不是 front,如果是就满了!!
第二、三问题怎么解决呢?
数组下标循环的小技巧
数组下标index 是 0、1、2、3、4、5、6、7
我们通过简单的运算就可以得到循环的下标
(index+1)% length
我们来试一试
当 index = 7 ,( index+1 )% length = 0
成功的实现了数组下标的循环操作
解决了这三个问题,我们来实现循环队列的具体方法
class MyCircularQueue {
private int front;
private int rear;
private int[] elem;
public MyCircularQueue(int k) {
// 构造方法
this.front = 0;
this.rear = 0; // 队首队尾下标都初始化为0
this.elem = new int[k]; // new 一个 k大小的数组
}
// 入队操作
public boolean enQueue(int value) {
// 如果队列已经满了,那么入队失败
if(isFull()) return false;
// 往rear 下标存放元素,之后rear向后走一格
this.elem[this.rear] = value;
this.rear = (this.rear+1)%this.elem.length;
return true;
}
public boolean deQueue() {
//出队操作
if(isEmpty()){
// 如果队列为空,那么出队失败
return false;
}
this.front = (this.front+1)%this.elem.length;
return true;
}
public int Front() {
// 得到队首元素
if(isEmpty()){
// 如果队列为空,那么返回-1
return -1;
}
return this.elem[this.front];
}
public int Rear() {
// 得到队尾元素
if(isEmpty()){
// 如果队列为空,那么返回-1
return -1;
}
// 如果队列不为空,那么返回rear的前一格元素,注意rear==0的情况
//rear == 0 的这种情况,前一个是数组的最后一个下标
int index = (this.rear ==0)? this.elem.length-1: this.rear-1;
return this.elem[index];
}
public boolean isEmpty() {
// 只要 rear 和 front 相遇时那么说明队列为空
if(this.rear==this.front){
return true;
}
return false;
}
public boolean isFull() {
// 如果front的前一格是rear 的话,那么就说明队列是满的
// 注意千万不要直接 front-1==rear,如果front ==0 ,那么就无法判断了
// 一定要考虑 front == 0 的这种情况,前一个是数组的最后一个下标
int index = (this.front ==0)? this.elem.length-1: this.front-1;
if(index == rear){
return true;
}
return false;
}
}
我们将这个代码进行OJ测试,
我们发现并没有通过测试,我们通过输出来看一下原因:
为什么会出现这样的结果呢?
在构造函数时传入的参数是3,预期结果是能够入三个元素才满,但是我们定义的循环队列必然会浪费一个空间,所以只能存两个元素,插入第三个元素的时候就失败了。
这个问题很好解决,我们在构造函数这里定义数组的空间大上一格,new int[k+1],这样这个队列中就可以插入k 个元素
public MyCircularQueue(int k) {
// 构造方法
this.front = 0;
this.rear = 0; // 队首队尾下标都初始化为0
this.elem = new int[k+1]; // new 一个 k+1大小的数组
}
再来提交OJ测试
通过测试!!
当然了,还有另外一种不需要浪费数组空间的做法
设置一个size来记录 队列中元素的大小
入队时size++
出队时size–
当size==0 ,队列为空
当size == elem.length ,此时队列已满
按照这个思路我们有了另外一种写法
代码展示
class MyCircularQueue {
private int front;
private int rear;
private int[] elem;
private int size;
public MyCircularQueue(int k) {
this.front = 0;
this.rear = 0;
this.elem = new int[k];
this.size = 0;
}
// 入队操作
public boolean enQueue(int value) {
// 如果队列已经满了,那么队列插入失败
if(isFull()){
return false;
}
// 如果队列未满,那么插入到数组中
this.elem[this.rear] = value;
this.rear++;
if(rear>=this.elem.length){
rear = 0;
}
this.size++;
return true;
}
public boolean isFull() {
if(this.size == this.elem.length){
return true;
}
return false;
}
public boolean deQueue() {
//出队操作
if(isEmpty()){
// 如果队列为空,那么出队失败
return false;
}
front++;
if(front>=this.elem.length){
front = 0;
}
this.size--;
return true;
}
public boolean isEmpty() {
if(this.size==0){
return true;
}
return false;
}
public int Front() {
// 得到队首元素
if(isEmpty()){
// 如果队列为空,那么返回-1
return -1;
}
return this.elem[this.front];
}
public int Rear() {
// 得到队尾元素
if(isEmpty()){
// 如果队列为空,那么返回-1
return -1;
}
// 如果队列不为空,那么返回rear的前一格元素,注意rear==0的情况
//rear == 0 的这种情况,前一个是数组的最后一个下标
int index = 0;
if(this.rear == 0){
index = this.elem.length-1;
}else{
index = rear-1;
}
return this.elem[index];
}
}
OJ测试结果:
通过测试!!
好了,这一节我们主要复习了队列的相关知识,了解了循环队列如何具体实现,如果感兴趣的同学可以自己实现一下双端队列的具体代码,希望大家多多练习!!
谢谢欣赏!