本篇博客所涉及到的代码,均已上传到github
上一篇通过Java实现二叉树的博客中:
数据结构与算法–使用Java实现二叉树
我们用到了一点队列的知识
本篇博文,将通过数组,单向循环链表两种方式,使用Java实现队列,
帮助大家进一步了解队列这种数据结构
本篇博客要点如下:
一. 队列
1.1 基本概念
队列: 先进先出(First in First Out)
队列是一种运算受限的线性表
只能在队列的一端进行插入,另一端进行删除
把插入元素的一端称为队尾,删除数据元素的一端称为队首
应用举例 :
开发中应用: 多线程中的就绪队列和阻塞队列
现实 : 各种排队场景
1.2 存储结构
包括顺序存储结构(数组)和链式存储结构(链表)
1.2.1 顺序存储结构
若使用顺序存储结构, 队列与ArrayList十分相似(限制ArrayList只能从一端插入,另一端删除,可以把它看做队列)
具体特征可以参考我之前写过的博客,这里不做过多赘述
1.2.2 链式存储结构
若用单链,可以参照这篇博客:
数据结构与算法–使用Java实现单链的LinkedList
若用双链,可以参照这篇博客:
数据结构与算法–使用Java实现循环双链的LinkedList
二. 使用Java代码实现队列
2.1 Queue接口
根据队列先进先出FIFO的特征
我们可以总结下关于队列的常用操作:
队列大小,队列非空判断, 元素入队,元素出队,获取队首元素
将这些操作抽象成方法,得到接口如下:
public interface Queue<E> {
// 返回队列的大小
int getSize();
// 队列是否为空
boolean isEmpty();
// 数据元素e入队
void enqueue(Object e);
// 数据元素出队
E dequeue();
// 取队首元素
E peek();
}
2.2 使用数组实现队列
首先定义两个成员变量:
private Object[] elementData; // 底层数据结构是数组
private int size; // 队列长度
给出构造方法:
public ArrayQueue() {
this(10); // 数组默认长度为10
}
public ArrayQueue(int size) {
elementData = new Object[size];
}
@Override
public int getSize() {
return size;
}
实现接口里的方法:
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void enqueue(Object e) {
if (size == elementData.length) {
// 首先队列满时,应该对队列进行扩容,扩容策略为size * 1.5
elementData = Arrays.copyOf(elementData, elementData.length * 3 / 2);
}
elementData[size] = e; // 从队列尾端插入元素
size++; // 队列长度 +1
}
@Override
public Object dequeue() {
if (isEmpty()) { // 队列为空时,不允许进行出队操作
System.out.println("队列长度为0,无法出队! ");
return null;
}
/*
先进先出,将后面的元素向前移位,原首元素置为空值
*/
Object first = elementData[0];
Object temp = elementData[size - 1];
for (int i = size - 1 ; i > 0; i --) {
Object data = elementData[i - 1];
elementData [i - 1] = temp;
temp = data;
}
elementData[size - 1] = null;
size --;
return first; //返回原首元素
}
@Override
public Object peek() {
return size > 0 ? elementData[0] : null; // 队列非空,返回首元素值,否则返回null
}
代码测试:
public static void main(String[] args) {
Queue queue = new ArrayQueue();
queue.dequeue(); // 在队列为长度为0的时候,尝试出队
queue.enqueue("3"); // 数据入队
System.out.println("队列长度: " + queue.getSize());
queue.enqueue("4");
queue.enqueue("5");
for (int i = 0; i < 100; i ++) {
queue.enqueue(i); // 数据入队,用于检测数组扩容机制是否正常
}
System.out.println("队首元素出队: " + queue.dequeue());
System.out.println("队列长度: " + queue.getSize());
System.out.println("队列队首元素为:" + queue.peek()); // 获取队列首端元素
}
运行结果:
从上面的代码中我们可以看到入队,获得队首元素等操作十分快捷
但是出队操作 需要将队首元素后面的元素全部前移一位,
时间复杂度为O(n),这种性能在生产中一定是不允许的
因此我们可以考虑使用链表实现
2.3 使用循环单链实现队列
在之前的博客中,
我介绍了单向链表,双向循环链表,这次我试着使用单向循环链表来实现队列
首先应该引入Node类
public class Node {
Object data; // 要存储的数据
Node next; // 指向下一个节点的指针
public Node(Object data) {
super();
this.data = data;
}
public Node(Object data, Node next) {
super();
this.data = data;
this.next = next;
}
public Node() {
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
定义必要的成员变量:
Node head; // 队首元素
Node tail; // 队尾元素
int size; // 队列长度
实现队列功能:
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void enqueue(Object e) {
Node t = tail; // 临时变量保存前队尾元素
Node node = new Node(e, head); // 新建节点,指针指向队首元素
if (t == null) { // 队列为空时,队首元素也是队尾元素
head = node;
tail = node;
} else {
t.next = node; // 前队尾元素指针指向新的队尾元素
tail = node; // 队尾元素更新
}
size++; // 对列长度增加
}
@Override
public Object dequeue() {
if (isEmpty()) {
System.out.println("队列长度为0,无法出队! ");
return null;
}
// 队列长度为1也属于特殊场景,需要单独处理
if (size == 1) {
Object data = head.data; // 定义临时变量储存头结点数据
head = null; // 队首结点置为空
size --; // 队列长度 -1
return data; // 返回原队首元素的值
}
// 队列长度大于1时
Node h = head;
head = null; // 原队首元素置为空值
size --; // 队列长度 -1
head = h.next; // 队首元素为原队首元素的下一个元素
tail.next = h.next; //将尾结点的指针指向原头结点的下一个节点即可
return h.data; // 返回原队首元素的值
}
@Override
public Object peek() {
return head == null ? null : head.data; // 队首元素为空的时候返回空值,否则返回队首元素的数据域
}
测试代码:
public static void main(String[] args) {
Queue queue = new LinkedQueue();
queue.dequeue(); // 在队列为长度为0的时候,尝试出队
queue.enqueue("3"); // 数据入队
System.out.println("队列长度为:" + queue.getSize());
queue.enqueue("4");
queue.enqueue("5");
for (int i = 0; i < 100; i ++) {
queue.enqueue(i); // 数据入队
queue.dequeue(); // 后面的数据入队,前面的数据出队
// 这里入队的顺序为 : 3, 4, 5 , 0, ...99
// 出队的顺序为 : 3, 4, 5, 0 ... 96
}
System.out.println("出队元素为: " + queue.dequeue());
System.out.println("队列长度为: " + queue.getSize());
System.out.println("队列首元素为: " + queue.peek()); // 获取队列首端元素
}
测试结果:
可以看到,结果是符合我们预期的!
从上面的代码中也可以看到,使用循环列表,入队,出队的时间复杂度均为O(1),性能很好!
这样,我们使用数组和单向循环链表实现了队列的基本功能!