数据结构之队列(Queue)
什么是队列(Queue)
之前总结过栈相关的知识,队列可以类比栈来看。栈只能在一端进行操作(栈顶),添加元素或者删除等都只能在栈顶;而队列有两端可以操作,在一端添加元素,在另一端删除元素。
我们把添加元素的一端称为队尾;删除元素的一端称为队首。
比如生活中的排队:城市中基本哪里都有,这就是一个队列。在队伍最前面就是队首,也是最先完成离开队伍的。新来的只能在队尾加入。
队列的特点:
- [ ] 相同数据类型元素构成的有序表(和栈一样)。
- [ ] 先进先出(FIFO:First In First Out)
- [ ] 队首删除元素,队尾插入元素。
队列操作及实现概述
实现
用链表、数组都比较容易实现队列。
链表实现:比较简单,在链表首部删除,链表尾部插入即可。
链表没有长度限制,不会造成溢出,而且插入删除也很简单,是很适合的实现方式。
数组实现:普通数组方式,有着明显的缺陷,长度固定、出队时剩下所有元素都要向前挪动一位,这种出队方式时间复杂度就是O(n)了。在此基础上进行一些处理,形成一个环,成为一个循环队列。
所以下面主要介绍的就是两种队列实现,用链表实现的队列 和 用数组实现的循环队列。
操作
队列主要两个操作,入队和出队。
入队(enQueue): 即在队尾插入新元素。
出队(deQueue): 即在队首删除一个元素。
如果自己实现,可以添加更多公共操作,如队长、是否队空或队满、查找等等。
下面实现中,为了更直观的显示出队、入队操作,都添加了一个displayQueue()的方法。
单链表队列实现
单链表队列实现很简单,链表首部即队首,链表尾部即队尾,定义两个引用分别指向队首和队尾,出队、入队直接删除和插入即可。时间复杂度为O(1)。
优势,没有额外的操作,没有长度的限制 不需担心溢出。
示意图大致如下:
具体代码如下(可结合注释,应该比较清晰):
public class QueueLinkedList<E> {
public static void main(String[] args) {
//入队1,2,3,4,5;然后出队2次
QueueLinkedList<Integer> queueLinkedList = new QueueLinkedList<Integer>();
queueLinkedList.displayQueue();
for (int i = 1; i < 6; i++) {
queueLinkedList.enQueue(Integer.valueOf(i));
}
queueLinkedList.displayQueue();
queueLinkedList.deQueue();
queueLinkedList.displayQueue();
queueLinkedList.deQueue();
queueLinkedList.displayQueue();
}
//定义队首引用,指向链表首部
private QueueNode<E> front;
//定义队尾引用,指向链表尾部
private QueueNode<E> rear;
//节点定义
static class QueueNode<E> {
E data;
QueueNode<E> next;
public QueueNode(E data) {
this.data = data;
}
}
//初始化,空的队列。
public QueueLinkedList() {
this.front = this.rear = null;
}
//入队,链表尾部插入
public void enQueue(E value) {
QueueNode<E> newNode = new QueueNode<E>(value);
//空队列时
if (this.rear == null) {
this.front = this.rear = newNode;
return;
}
this.rear.next = newNode;
this.rear = newNode;
}
//出队,链表首部删除
public E deQueue() {
//队列为空时
if (this.front == null)
return null;
E result = this.front.data;
this.front = this.front.next;
//队列中只有一个元素时
if (this.front == null)
this.rear = null;
return result;
}
//打印队列所有元素,以及队首、队尾信息
public void displayQueue() {
if (this.front == null) {
System.out.println("front:null<-...<-rear:null");
System.out.println("This is an empty queue!");
System.out.println();
return;
}
System.out.println("front:" + this.front.data +"<-...<-rear:" + this.rear.data);
QueueNode<E> tmpNode = this.front;
while (tmpNode != null) {
System.out.print(tmpNode.data + "<-");
tmpNode = tmpNode.next;
}
System.out.println();
System.out.println();
}
}
实验结果为:
front:null<-...<-rear:null
This is an empty queue!
front:1<-...<-rear:5
1<-2<-3<-4<-5<-
front:2<-...<-rear:5
2<-3<-4<-5<-
front:3<-...<-rear:5
3<-4<-5<-
数组循环队列实现
若用普通数组实现队列,就会发现每次删除队首时所有元素都需要向前移动一位,这样实现肯定是不合适的。
于是,在上述基础上做一些处理,便形成了循环队列:将数组队列看出一个首尾相连的环形。用front和rear表示队首和队尾相关元素的下标(不一定是front是队首下标,rear是队尾下表),这样就能很容易确定队列的元素是哪些了。出队、入队改变这两个值即可而不需要移动数组中元素了。
下面实现用变量front和rear标识队首、队尾的下标,入队和出队的方式基本一致:数组大小为capacity,即front=(font+1)%capacity 或 rear=(rear+1)%capacity。
也可以用front标识队首,rear标识队尾的下一个位置,只是不同的标识含义, 初始化 和 队列为空 及 队列为满 的判断和处理需要注意,是不同的。 (rear标识队尾下一个位置时,初始化front=rear=0,空即front==rear。而rear标识队尾即下面的实现,可以具体看下)
大致示意图如下(front和rear标识队首、队尾的下标):
实现代码(可结合注释查看):
注:实现中的判空与判满 使用了另一个变量queueCount表示队列元素个数,便于返回队列大小。也可以使用front和rear之间关系进行判断
public class QueueCircleArray {
public static void main(String[] args) {
//队列入队1,2,3,4;然后两次出队;然后入队10,20
QueueCircleArray queueCircleArray = new QueueCircleArray(4);
queueCircleArray.displayQueue();
for (int i = 1; i < 5; i++) {
queueCircleArray.enQueue(Integer.valueOf(i));
}
queueCircleArray.displayQueue();
queueCircleArray.deQueue();
queueCircleArray.deQueue();
queueCircleArray.displayQueue();
queueCircleArray.enQueue(Integer.valueOf(10));
queueCircleArray.enQueue(Integer.valueOf(20));
queueCircleArray.displayQueue();
}
//数组及大小
private Object[] array;
private int capacity;
//队首 队尾对应数组中的下标
private int front, rear;
//队列的大小
private int queueCount;
public QueueCircleArray() {
this(10);
}
public QueueCircleArray(int capacity) {
this.capacity = capacity;
this.front = queueCount = 0;
//注意这里初始化
this.rear = this.capacity - 1;
this.array = new Object[this.capacity];
}
public boolean isFull() {
return this.queueCount == this.capacity;
}
public boolean isEmpty() {
return this.queueCount == 0;
}
public void enQueue(Object value) {
if (isFull())
throw new RuntimeException("Queue is Full!");
//队尾向后移动一位,即将插入数据的位置
this.rear = (this.rear+1)%this.capacity;
//插入数据,入队
this.array[this.rear] = value;
//队列大小加1
this.queueCount++;
}
public Object deQueue() {
if (isEmpty())
throw new RuntimeException("Queue is Empty!");
Object tmpObject = this.array[this.front];
//队首向后移动一位,之前的位置即出队的元素。
this.front = (this.front+1)%this.capacity;
//队列大小减1
this.queueCount--;
return tmpObject;
}
public void displayQueue() {
if (isEmpty()) {
System.out.println("front:null<-...<-rear:null");
System.out.println("This is an empty queue!");
System.out.println();
return;
}
int tmp = this.front;
System.out.println("front:["+ this.front + "]" + this.array[this.front]
+"<-...<-rear:[" + this.rear + "]" + this.array[this.rear]);
while(tmp != this.rear) {
System.out.print(this.array[tmp] + "<-");
tmp = (tmp + 1)%this.capacity;
}
if (tmp == this.rear)
System.out.println(this.array[tmp]);
System.out.println();
}
}
实验结果:
front:null<-...<-rear:null
This is an empty queue!
front:[0]1<-...<-rear:[3]4
1<-2<-3<-4
front:[2]3<-...<-rear:[3]4
3<-4
front:[2]3<-...<-rear:[1]20
3<-4<-10<-20