队列(queue):
what(什么是队列)
队列同样是一种特殊的线性表,其插入和删除的操作分别在表的两端进行,队列的特点就是先进先出(First In First Out)。我们把向队列中插入元素的过程称为入队(Enqueue),删除元素的过程称为出队(Dequeue)并把允许入队的一端称为队尾,允许出的的一端称为队头,没有任何元素的队列则称为空队。其一般结构如下:
package com.zejian.structures.Queue;
/**
* Created by zejian on 2016/11/28.
* Blog :http://blog.csdn.net/javazejian/article/details/53375004 [原文地址,请尊重原创]
* 队列抽象数据类型
*/
public interface Queue<T> {
/**
* 返回队列长度
* @return
*/
int size();
/**
* 判断队列是否为空
* @return
*/
boolean isEmpty();
/**
* data 入队,添加成功返回true,否则返回false,可扩容
* @param data
* @return
*/
boolean add(T data);
/**
* offer 方法可插入一个元素,这与add 方法不同,
* 该方法只能通过抛出未经检查的异常使添加元素失败。
* 而不是出现异常的情况,例如在容量固定(有界)的队列中
* NullPointerException:data==null时抛出
* @param data
* @return
*/
boolean offer(T data);
/**
* 返回队头元素,不执行删除操作,若队列为空,返回null
* @return
*/
T peek();
/**
* 返回队头元素,不执行删除操作,若队列为空,抛出异常:NoSuchElementException
* @return
*/
T element();
/**
* 出队,执行删除操作,返回队头元素,若队列为空,返回null
* @return
*/
T poll();
/**
* 出队,执行删除操作,若队列为空,抛出异常:NoSuchElementException
* @return
*/
T remove();
/**
* 清空队列
*/
void clearQueue();
}
顺序队列的实现:
入队操作直接执行顺序表尾部插入操作,其时间复杂度为O(1),出队操作直接执行顺序表头部删除操作,其时间复杂度为O(n),主要用于移动元素,效率低,既然如此,我们就把出队的时间复杂度降为O(1)即可,为此在顺序表中添加一个头指向下标front和尾指向下标,出队和入队时只要改变front、rear的下标指向取值即可,此时无需移动元素,因此出队的时间复杂度也就变为O(1)。
从图的演示过程,(a)操作时,是空队列此时front和rear都为-1,同时可以发现虽然我们通过给顺序表添加front和rear变量记录下标后使用得出队操作的时间复杂度降为O(1),但是却出现了另外一个严重的问题,那就是空间浪费,从图中的(d)和(e)操作可以发现,20和30出队后,遗留下来的空间并没有被重新利用,反而是空着,所以导致执行(f)操作时,出现队列已满的假现象,这种假现象我们称之为假溢出,之所以出现这样假溢出的现象是因为顺序表队列的存储单元没有重复利用机制,而解决该问题的最合适的方式就是将顺序队列设计为循环结构,接下来我们就通过循环顺序表来实现顺序队列。
顺序循环队列就是将顺序队列设计为在逻辑结构上收尾相接的循环结构,这样我们就可以重复利用存储单元,其过程如下所示:
简单分析一下:
其中采用循环结构的顺序表,可以循环利用存储单元,因此有如下计算关系(其中size为队列长度):
//其中front、rear的下标的取值范围是0~size-1,不会造成假溢出。
front=(front+1)%size;//队头下标
rear=(rear+1)%size;
front为队头元素的下标,rear则指向下一个入队元素的下标
当front=rear时,我们约定队列为空。
出队操作改变front下标指向,入队操作改变rear下标指向,size代表队列容量。
约定队列满的条件为front=(rear+1)%size,注意此时队列中仍有一个空的位置,此处留一个空位主要用于避免与队列空的条件front=rear相同。
队列内部的数组可扩容,并按照原来队列的次序复制元素数组
了解了队列的实现规则后,我们重点分析一下入队offer方法和出队poll方法,其中入队add方法实现如下:
/**
* 扩容的方法
* @param capacity
*/
public void ensureCapacity(int capacity) {
//如果需要拓展的容量比现在数组的容量还小,则无需扩容
if (capacity<size)
return;
T[] old = elementData;
elementData= (T[]) new Object[capacity];
int j=0;
//复制元素
for (int i=this.front; i!=this.rear ; i=(i+1)%old.length) {
elementData[j++] = old[i];
}
//恢复front,rear指向
this.front=0;
this.rear=j;
}
这个方法比较简单,主要创建一个新容量的数组,并把旧数组中的元素复制到新的数组中,这里唯一的要注意的是,判断旧数组是否复制完成的条件为i!=this.rear,同时循环的自增条件为i=(i+1)%old.length。扩容后直接通过rear添加新元素,最后更新rear指向下一个入队新元素。对于出队操作poll的实现如下:
/**
* 出队,执行删除操作,返回队头元素,若队列为空,返回null
* @return
*/
@Override
public T poll() {
T temp=this.elementData[this.front];
this.front=(this.front+1)%this.elementData.length;
size--;
return temp;
}
出队操作相对简单些,直接存储要删除元素的数据,并更新队头front的值,最后返回删除元素的数据。ok~,关于循环结构的顺序队列,我们就分析到此,最后给出循环顺序队列的实现源码,其他方法比较简单,注释也很清楚
顺序队列源码:
package com.zejian.structures.Queue;
import java.io.Serializable;
import java.util.NoSuchElementException;
/**
* Created by zejian on 2016/11/28.
* Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
* 顺序队列的实现
*/
public class SeqQueue<T> implements Queue<T> ,Serializable {
private static final long serialVersionUID = -1664818681270068094L;
private static final int DEFAULT_SIZE = 10;
private T elementData[];
private int front,rear;
private int size;
public SeqQueue(){
elementData= (T[]) new Object[DEFAULT_SIZE];
front=rear=0;
}
public SeqQueue(int capacity){
elementData= (T[]) new Object[capacity];
front=rear=0;
}
@Override
public int size() {
// LinkedList
return size;
}
@Override
public boolean isEmpty() {
return front==rear;
}
/**
* data 入队,添加成功返回true,否则返回false,可扩容
* @param data
* @return
*/
@Override
public boolean add(T data) {
//判断是否满队
if (this.front==(this.rear+1)%this.elementData.length){
ensureCapacity(elementData.length*2+1);
}
//添加data
elementData[this.rear]=data;
//更新rear指向下一个空元素的位置
this.rear=(this.rear+1)%elementData.length;
size++;
return true;
}
/**
* offer 方法可插入一个元素,这与add 方法不同,
* 该方法只能通过抛出未经检查的异常使添加元素失败。
* 而不是出现异常的情况,例如在容量固定(有界)的队列中
* NullPointerException:data==null时抛出
* IllegalArgumentException:队满,使用该方法可以使Queue的容量固定
* @param data
* @return
*/
@Override
public boolean offer(T data) {
if (data==null)
throw new NullPointerException("The data can\'t be null");
//队满抛出异常
if (this.front==(this.rear+1)%this.elementData.length){
throw new IllegalArgumentException("The capacity of SeqQueue has reached its maximum");
}
//添加data
elementData[this.rear]=data;
//更新rear指向下一个空元素的位置
this.rear=(this.rear+1)%elementData.length;
size++;
return true;
}
/**
* 返回队头元素,不执行删除操作,若队列为空,返回null
* @return
*/
@Override
public T peek() {
return elementData[front];
}
/**
* 返回队头元素,不执行删除操作,若队列为空,抛出异常:NoSuchElementException
* @return
*/
@Override
public T element() {
if(isEmpty()){
throw new NoSuchElementException("The SeqQueue is empty");
}
return peek();
}
/**
* 出队,执行删除操作,返回队头元素,若队列为空,返回null
* @return
*/
@Override
public T poll() {
T temp=this.elementData[this.front];
this.front=(this.front+1)%this.elementData.length;
size--;
return temp;
}
/**
* 出队,执行删除操作,若队列为空,抛出异常:NoSuchElementException
* @return
*/
@Override
public T remove() {
if (isEmpty()){
throw new NoSuchElementException("The SeqQueue is empty");
}
return poll();
}
@Override
public void clearQueue() {
for (int i=this.front; i!=this.rear ; i=(i+1)%elementData.length) {
elementData[i] = null;
}
//复位
this.front=this.rear=0;
size=0;
}
/**
* 扩容的方法
* @param capacity
*/
public void ensureCapacity(int capacity) {
//如果需要拓展的容量比现在数组的容量还小,则无需扩容
if (capacity<size)
return;
T[] old = elementData;
elementData= (T[]) new Object[capacity];
int j=0;
//复制元素
for (int i=this.front; i!=this.rear ; i=(i+1)%old.length) {
elementData[j++] = old[i];
}
//恢复front,rear指向
this.front=0;
this.rear=j;
}
}
链式队列的设计与实现
………..
why(为什么用队列)
how(怎么用)
队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作.(先进先出)
LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。
import java.util.LinkedList;
import java.util.Queue;
import org.junit.Before;
import org.junit.Test;
/**
* 队列测试:实现类使用LinkedList
*
* Queue也有很多其他的实现类,比如java.util.concurrent.LinkedBlockingQueue。
* LinkedBlockingQueue是一个阻塞的线程安全的队列,底层实现也是使用链式结构。
*/
public class TestQuene {
// 定义一个队列
Queue<String> queue;
@Before
public void before() {
// 实例化队列变量
queue = new LinkedList<String>();
// add方法向队列中添加元素,返回布尔值,add方法添加失败时会抛异常,不推荐使用
// queue.add("1");
// queue.add("2");
// queue.add("3");
// queue.add("4");
// queue.add("5");
// offer方法向队列中添加元素,返回布尔值
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d");
queue.offer("e");
}
// poll方法移除队列首个元素并返回,若队列为空,返回null
@Test
public void test1() {
// 弹出元素
String pollEle = queue.poll(); // 先进先出,弹出了a
System.out.println(pollEle); // a
System.out.println(queue); // [b, c, d, e]
}
// remove方法移除首个元素并返回,若队列为空,会抛出异常:NoSuchElementException,不推荐使用
@Test
public void test2() {
// 弹出元素
String remove = queue.remove(); // 先进先出,弹出了a
System.out.println(remove); // a
System.out.println(queue); // [b, c, d, e]
}
// peek方法返回队列首个元素,但不移除,若队列为空,返回null
@Test
public void test3() {
// 查看首个元素
String peek = queue.peek(); // 首个元素是a,最先加入
System.out.println(peek); // a
System.out.println(queue); // [a, b, c, d, e]
}
// element方法返回队列的头元素,但不移除,若队列为空,会抛出异常:NoSuchElementException,不推荐使用
@Test
public void test4() {
// 查看首个元素
String element = queue.element();
System.out.println(element); // a
System.out.println(queue); // [a, b, c, d, e]
}
}
注意:在这里使用testNG测试