队列是一种常见的数据结构。队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
上述内容取自于百度百科,也简单的介绍了下队列是什么样的一种结构。这种线性表的规则就是FIFO(First in first out),谁是第一个进去的,谁就第一个出来。这里可能把他想象成一根直的管道。左边是出口,只能出,右边是入口,只能进入。这就是一个简单的队列模型。当然,其实队列是基于双向链表来实现的,只是它在双向链表的基础上,进行了一些限定。一端不允许进入,一端不允许输出。下面就是简单的一个队列的图。
首先是队列的图形:
(
形象得看出,出口方向在左边,出口则是在右边。进栈顺序则是,12345,那么front是什么意思呢,代表的是将要出列的元素的位置,而rear就是下一个要插入元素的位置。那么来插入一个元素:
插入一个6之后,rear增加到下一个位置,而没有执行出列操作,front不变。接着继续执行出列操作。
这里我们可以看到,出列后,rear指向的位置后移了一个,而这个操作适合front无关的。这边是队列queue这种数据结构的特征。而我们通过搜索可以发现,queue在java中只是一个借口,而下面也没有具体的实现类。这点与stack是不同的,那么我们便需要自己来实现这个接口,同时来复写这个接口的方法。
第一种方法便是使用我们已经写好的Array,也就是增强数组,作为底层的结构。代码如下:
package com.it.data;
public class ArrayQueue<E>
{
private Array<E> queue;
public ArrayQueue(){
queue = new Array<>();
}
public ArrayQueue(int capacity){
queue = new Array<>(capacity);
}
public boolean offer(E e){
queue.addLast(e);
return true;
}
public E poll(){
return queue.delFirst();
}
public E peek(){
return queue.get(queue.getSize()-1);
}
public int getSize(){
return queue.getSize();
}
public int getCapacity() {
return queue.getCapacity();
}
public static void main(String args[]){
ArrayQueue arr = new ArrayQueue(3);
arr.offer(1);
arr.offer(2);
arr.offer(3);
arr.offer(4);
arr.offer(5);
System.out.println(arr.getCapacity());
int j = arr.getSize();
for (int i = 0; i<j; i++) {
System.out.println(arr.poll());
}
//会抛出runtime异常,因为此时数组已经空了。
arr.poll();
}
}
这个代码就刨除了front以及rear这两个指针变量,那么,他没有这两个指针,如何确定增加以及删除的位置呢?
那么从时间复杂度上来说,每次增加元素都为O(1),但是单次删除元素的操作就为O(n),如果这个链表大小为上千甚至上亿,那么这个时间就有些吓人了。
所以我们需要将删除的这个操作优化一下,使用链式的队列来完成。这时候我们就不能在调用自己写的Array数组了,因为Array数组中的delFirst操作,就是将每个数组的位置来进行移动。
public void addLast(T t){
add(size,t);
}
public void add(int index,T t){
if(index>size || index<0)
throw new IllegalArgumentException("插入角标违法,请检查后再输入");
if (this.getCapacity() == this.getSize()) {
capactyInc(this.getCapacity()*2);
}
for (int i = index; i < size ; i++) {
data[i+1] = data[i];
}
data[index] = t;
size++;
}
接下来我们直接使用一维数组,来进行链式的队列操作。这样我们在移除元素时,就不需要再将每个元素向前移动一维。具体的示意图如下:
当然这样的节奏也有一些问题,首先,当数组满的时候,rear会重新到数组头,呢么,rear和front是相等的,可是这个时候,队列真的已经满了吗,那可不一定,在我们刚创立数组的时候,也会出现front和rear同时指向一个地址的情况,这个时候,便可以空出一个位置,来进行判断,也就是说,当rear+1为front的时候,我们便判断这个时候数组已经满了,这个问题便可以解决了。
下面有Java代码来实现:
package com.it.data;
public class QueueByArray<T> {
//内部维护的数组
private T[] arr;
//控制删除元素的指针
private int front;
//控制插入元素的指针
private int rear;
public QueueByArray(){
this(15);
}
public QueueByArray(int capatity){
arr = (T[])new Object[capatity+1];
}
//进队列
public int offer(T t){
if(rear+1 == arr.length ) {
if (front == 0) {
throw new IllegalArgumentException("队列已满,无法插入");
} else {
arr[rear] = t;
rear = 0;
}
}else{
arr[rear] = t;
rear++;
}
return rear;
}
//出队列
public T poll(){
if (rear == front){
throw new IllegalArgumentException("队列中暂无数据");
}
T temp = arr[front];
arr[front] = null;
if(front == arr.length -1 ){
front=0;
}else {
front++;
}
return temp;
}
//查询最新元素
public T peek(){
if(rear==front){
throw new IllegalArgumentException("队列中暂无数据");
}else if(rear == 0 ){
return arr[arr.length];
}else{
return arr[rear-1];
}
}
public static void main(String args[]){
QueueByArray qba = new QueueByArray(3);
for (int i = 0; i < 3; i++) {
qba.offer(1);
System.out.println(i);
}
for(int i = 0;i<4 ; i++){
qba.poll();
}
}
}
再看下输出结果:
0
1
2
Exception in thread "main" java.lang.IllegalArgumentException: 队列中暂无数据
at com.it.data.QueueByArray.poll(QueueByArray.java:37)
at com.it.data.QueueByArray.main(QueueByArray.java:67)
当然,用这种抛出异常的方法处理其实是很不合适的,这时候我们可以抛出一个null,然后指针不变也可以。