一,前言
队列是一种只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表。其具有先进先出的特性,入队的一端称之为队尾,出队的一端称之为队头,结构如下图所示:
二,基本实现
队列根据底层存储结构不同分为顺序结构和链式结构,本文模仿Java集合类中的实现方式,采用双链表作为基本存储结构。
1,接口定义
本次实现选择传递泛型,故接口类 也写作泛型类。
java
public interface IQueue<T> {
// 入队
boolean offer(T val);
// 出队
T poll();
// 获取队头元素
T peek();
// 获取队列中有效元素个数
int size();
// 检测队列是否为空
boolean isEmpty();
}
2,实现类定义
QueueImpl实现类同样定义为泛型类并实现IQueue<T>接口,定义size变量存储队列中有效元素个数,定义Node<T>节点内部类作为基本存储结构,定义first/last引用指向队头和队尾,并提供无参构造方法对所有成员变量进行初始化赋值。
java
public class QueueImpl<T> implements IQueue<T> {
private int size;
private Node<T> first;
private Node<T> last;
/**
* 无参构造方法*/
public QueueImpl() {
this.size = 0;
this.first = null;
this.last = null;
}
/**
* 节点内部类*/
private static class Node<T> {
T val;
Node<T> next;
Node<T> prev;
Node(T val) {
this.val = val;
}
}
}
3,主要方法实现
<1> 入队
在入队时,根据队列容量不同,会存在以下两种情况:
1,队列为空 ==> 将头尾指针同时指向新节点
2,队列不为空 ==> 尾插新节点,如下图所示:

java
/**
* 入队*/
@Override
public boolean offer(T val) {
// 准备待插入节点
Node<T> node = new Node<>(val);
// 根据容量确定插入方式
if(isEmpty()) {
this.first = node;
this.last = node;
} else {
this.last.next = node;
node.prev = this.last;
this.last = node;
}
// 维护队列有效元素个数
this.size ++;
return true;
}
<2> 出队
在出队时,根据队列容量不同,会存在以下两种情况:
1,队列为空 ==> 抛出异常
2,队列不为空 ==> 头删,如下图所示:

java
/**
* 出队*/
@Override
public T poll() {
// 判空
if(isEmpty()) {
throw new QueueEmptyException("队列为空,无法执行出队操作!");
}
// 根据容量确定删除方式 并 保留出队元素
T oldVal = this.first.val;
if(this.size == 1) {
this.first = null;
this.last = null;
} else {
this.first = this.first.next;
this.first.prev = null;
}
// 维护队列有效元素个数
this.size --;
return oldVal;
}
<3> 获取队头元素
在获取队头元素时,根据队列容量不同,会存在以下两种情况:
1,队列为空 ==> 抛出异常
2,队列不为空 ==> 返回头节点值
java
/**
* 获取队头元素*/
@Override
public T peek() {
// 判空
if(isEmpty()) {
throw new QueueEmptyException("队列为空,无法执行获取队头元素操作!");
}
return this.first.val;
}
三,循环队列
在实际使用中,我们有时还会接触到循环队列,如操作系统中的生产者消费者模型。而循环队列通常使用数组实现,其大致逻辑结构如下图所示:

1,实现逻辑
<1> 步进
为了能够使得元素在数组中循环存储,即在数组末尾步进后重新回到数组开头,故下一步下标计算公式为:
index = (index + 1) % n; (其中n为数组长度,+1为步进长度)
如下图所示:
<2> 判空与判满
在循环队列中,若是不加任何处理,那么队列满和队列空时首尾指针都会指向同一个位置,此时就难以判断队列的存储状态,故我们可以通过以下几种方式进行处理:
- 通过添加size属性记录
- 保留一个位置用于判断
- 使用标记
第一种和第三种都很容易理解,第二种的处理逻辑如下:
空出一个位置不存放元素作为标记。
当首尾指针指向同一个位置时,队列为空;
当(尾指针 + 1) % 数组长度 == 头指针时,队列为满,如下图所示。
2,基础实现
java
class MyCircularQueue {
/**
* 设计一个循环队列
* 判满/判空:使用usedSize计数器
* 步进:(i + 1)% n
* front表示队头下标,rear表示可插入位置节点*/
private int[] queue;
private int size;
private int front;
private int rear;
private int k;
/**
* 构造方法*/
public MyCircularQueue(int k) {
this.queue = new int[k];
this.size = 0;
this.front = 0;
this.rear = 0;
this.k = k;
}
/**
* 入队*/
public boolean enQueue(int value) {
if(isFull()) {
return false;
} else {
// -rear指向可插入位置,故先插入后步进
this.queue[this.rear] = value;
this.rear = (this.rear + 1) % this.k;
this.size ++;
return true;
}
}
/**
* 出队*/
public boolean deQueue() {
if(isEmpty()) {
return false;
} else {
this.front = (this.front + 1) % this.k;
this.size --;
return true;
}
}
/**
* 获取队头元素*/
public int Front() {
if(isEmpty()) {
return -1;
} else {
return this.queue[this.front];
}
}
/**
* 获取队尾元素*/
public int Rear() {
if(isEmpty()) {
return -1;
} else {
if(this.rear == 0) {
return this.queue[this.k - 1];
} else {
return this.queue[this.rear - 1];
}
}
}
/**
* 判空*/
public boolean isEmpty() {
return this.size == 0;
}
/**
* 判满*/
public boolean isFull() {
return this.size == this.k;
}
}
四,双端队列 Deque
双端队列是指两端都允许进行入队和出队操作的队列,其在Java集合类中被定义为接口Deque,当使用该类型创建队列时,我们所获得的就是双端队列。创建代码如下:
java
Deque<Integer> queue1 = new ArrayDeque<>(); // 基于可变数组的顺序存储实现
Deque<Integer> queue2 = new LinkedList<>(); // 基于双向链表的链式存储实现
Deque不仅可以作为普通队列使用,还可以作为栈使用,并且可以用于实现单调队列等算法。

