一、队列的定义
队列 是一种先进先出的线性表。其限制仅在表的一端(尾端)进行插入,另一端(首端)进行删除的线性表,先进先出FIFO。
不管对于出队还是入队,front自增或者rear自增
在java5中新增加了java.util.Queue接口,用以支持队列的常见操作。Queue接口与List、Set同一级别,都是继承了Collection接口。
Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。 如果要使用前端而不移出该元素,使用
element()或者peek()方法。
值得注意的是LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。
二、队列Java实现
1、基于数组
package com.wqc.stack;
public class ArrayQueue<E> {
private Object[] data = null;
private int maxSize; //队列容量
private int front; //队列头,允许删除
private int rear; //队列尾,允许插入
public ArrayQueue(){
this(10);
}
public ArrayQueue(int initialSize) {
if(initialSize > 0) {
data = new Object[initialSize];
this.maxSize = initialSize;
front = rear = 0;
}else{
throw new RuntimeException("初始化大小不能小于0:" + initialSize);
}
}
//判空
public boolean isEmpty() {
return rear == front && rear == 0;
}
//入队
public boolean add(E e) {
if (rear == maxSize) {
throw new RuntimeException("队列已满,无法插入新的元素");
}else {
data[rear] = e;
rear ++;
return true;
}
}
//出队
public E poll(){
if (isEmpty()) {
throw new RuntimeException("空队列异常!");
}else {
E value = (E)data[front];
data[front++] = null; //为什么是front++,而不是front--,因为队列为空时,front为0,
//初始化完成后,队列头front还是为0,只不过rear为maxSize,当做出队
//操作时,front向rear靠拢,于是为front++
return value;
}
}
public int length() {
return rear - front;
}
public void display() {
for (Object object : data) {
System.out.println(object);
}
}
public static void main(String[] args) {
ArrayQueue<Integer> arrayQueue = new ArrayQueue();
arrayQueue.add(1);
arrayQueue.add(2);
arrayQueue.add(3);
arrayQueue.add(4);
arrayQueue.add(5);
arrayQueue.add(6);
arrayQueue.add(7);
arrayQueue.add(8);
arrayQueue.display();
System.out.println(arrayQueue.length());
System.out.println(arrayQueue.poll());
System.out.println(arrayQueue.length());
}
}
2、基于链表
链式队列的设计与实现,对于链式队列,将使用带头指针front和尾指针rear的单链表实现,front直接指向队头的第一个元素,rear指向队尾的最后一个元素,其结构如下:
之所以选择单链表(带头尾指针)而不采用循环双链表或者双链表主要是双链表的空间开销(空间复杂度,多前继指针)相对单链表来说大了不少,而单链表只要新增头指针和尾指针就可以轻松实现常数时间内(时间复杂度为O(1))访问头尾结点。下面我们来看看如何设计链式队列:
- 以上述的图为例分别设置front和rear指向队头结点和队尾结点,使用单链表的头尾访问时间复杂度为O(1)。
- 设置初始化空队列,使用front=rear=null,并且约定条件
front==null&&rear==null
- 成立时,队列为空。
- 出队操作时,若队列不为空获取队头结点元素,并删除队头结点元素,更新front指针的指向为front=front.next
- 入队操作时,使插入元素的结点在rear之后并更新rear指针指向新插入元素。
- 当第一个元素入队或者最后一个元素出队时,同时更新front指针和rear指针的指向。
这一系列过程如下图所示:
package com.wqc.stack;
public class LinkQueue {
// 链栈的节点
private class Node<E> {
E e;
Node<E> next;
public Node() {
}
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
}
private Node front;// 队列头,允许删除
private Node rear;// 队列尾,允许插入
private int size; //队列当前长度
public LinkQueue() {
front = null;
rear = null;
}
//判空
public boolean empty(){
return size==0;
}
//插入
public boolean add(E e){
if(empty()){ //如果队列为空
front = new Node(e,null);//只有一个节点,front、rear都指向该节点
rear = front;
}else{
Node<E> newNode = new Node<E>(e, null);
rear.next = newNode; //让尾节点的next指向新增的节点
rear = newNode; //以新节点作为新的尾节点
}
size ++;
return true;
}
//返回队首元素,但不删除
public Node<E> peek(){
if(empty()){
throw new RuntimeException("空队列异常!");
}else{
return front;
}
}
//出队
public Node<E> poll(){
if(empty()){
throw new RuntimeException("空队列异常!");
}else{
Node<E> value = front; //得到队列头元素
front = front.next;//让front引用指向原队列头元素的下一个元素
value.next = null; //释放原队列头元素的next引用
size --;
return value;
}
}
//队列长度
public int length(){
return size;
}
}
3、基于LinkedList实现队列结构
/**
* 使用java.util.Queue接口,其底层关联到一个LinkedList(双端队列)实例.
*/
import java.util.LinkedList;
import java.util.Queue;
public class QueueList<E> {
private Queue<E> queue = new LinkedList<E>();
// 将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,
//如果当前没有可用的空间,则抛出 IllegalStateException。
public boolean add(E e){
return queue.add(e);
}
//获取,但是不移除此队列的头。
public E element(){
return queue.element();
}
//将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,
//此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。
public boolean offer(E e){
return queue.offer(e);
}
//获取但不移除此队列的头;如果此队列为空,则返回 null
public E peek(){
return queue.peek();
}
//获取并移除此队列的头,如果此队列为空,则返回 null
public E poll(){
return queue.poll();
}
//获取并移除此队列的头
public E remove(){
return queue.remove();
}
//判空
public boolean empty() {
return queue.isEmpty();
}
}
三、普通队列与循环队列
1、普通队列的缺点。
使用顺序表作为底层容器来实现。实际上采用顺序表实现队列时,入队操作直接执行顺序表尾部插入操作,其时间复杂度为O(1),出队操作直接执行顺序表头部删除操作,其时间复杂度为O(n),主要用于移动元素,效率低,既然如此,我们就把出队的时间复杂度降为O(1)即可,为此在顺序表中添加一个头指向下标front和尾指向下标,出队和入队时只要改变front、rear的下标指向取值即可,此时无需移动元素,因此出队的时间复杂度也就变为O(1)。其过程如下图所示
从图的演示过程,(a)操作时,是空队列此时front和rear都为-1,同时可以发现虽然我们通过给顺序表添加front和rear变量记录下标后使用得出队操作的时间复杂度降为O(1),但是却出现了另外一个严重的问题,那就是空间浪费,从图中的(d)和(e)操作可以发现,20和30出队后,遗留下来的空间并没有被重新利用,反而是空着,所以导致执行(f)操作时,出现队列已满的假现象,这种假现象我们称之为假溢出,之所以出现这样假溢出的现象是因为顺序表队列的存储单元没有重复利用机制,而解决该问题的最合适的方式就是将顺序队列设计为循环结构,接下来我们就通过循环顺序表来实现顺序队列。
2、循环队列
顺序循环队列就是将顺序队列设计为在逻辑结构上收尾相接的循环结构,这样我们就可以重复利用存储单元,其过程如下所示:
import java.util.Arrays;
public class LoopQueue<E> {
public Object[] data = null;
private int maxSize; // 队列容量
private int rear;// 队列尾,允许插入
private int front;// 队列头,允许删除
private int size=0; //队列当前长度
public LoopQueue() {
this(10);
}
public LoopQueue(int initialSize) {
if (initialSize >= 0) {
this.maxSize = initialSize;
data = new Object[initialSize];
front = rear = 0;
} else {
throw new RuntimeException("初始化大小不能小于0:" + initialSize);
}
}
// 判空
public boolean empty() {
return size == 0;
}
// 插入
public boolean add(E e) {
if (size == maxSize) {
throw new RuntimeException("队列已满,无法插入新的元素!");
} else {
data[rear] = e;
rear = (rear + 1)%maxSize;
size ++;
return true;
}
}
// 返回队首元素,但不删除
public E peek() {
if (empty()) {
throw new RuntimeException("空队列异常!");
} else {
return (E) data[front];
}
}
// 出队
public E poll() {
if (empty()) {
throw new RuntimeException("空队列异常!");
} else {
E value = (E) data[front]; // 保留队列的front端的元素的值
data[front] = null; // 释放队列的front端的元素
front = (front+1)%maxSize; //队首指针加1
size--;
return value;
}
}
// 队列长度
public int length() {
return size;
}
//清空循环队列
public void clear(){
Arrays.fill(data, null);
size = 0;
front = 0;
rear = 0;
}
}