队列(queue)和我们生活中的队伍一样,是一种等待服务的模式,就是站到队列中去。它也是一种数据结构,和栈一样,也是作为程序员的一种辅助工具,但是栈是先进后出,而队列是先进先出,这和我们现实世界的排队是一样的,它可以被用在一些复杂的数据结构算法中,比如图,也可以用于模拟真实世界的环境,比如模拟电影院的排队取票等。
在计算机操作系统当中,队列也有其用武之地。打印作业在打印队列中等待着打印操作。同时在键盘敲击内容到计算机时,若计算机要做其他工作,那么将会利用一个存储键入内容的队列来存储键入值,在每敲击一些键盘时,便会有一个值被存入到队列中。存在队列的数据不会丢失,并且是有序的,它会等待等待文字读取程序来读取它。利用队列来保存这些数据的优点就在于,存储的数据始终是有序的。
队列是如何工作的
队列可以使用数组来实现,也可以使用链表来实现,由于链表还未介绍,所以一些内容队列均匀数组实现。
队列的基本操作有两种,插入和移除。在栈当中这两个操作都有标准的名字:push和pop,但是队列没有,插入可以叫做insert、push和enque,移除可以叫做delete、pop和deque,队尾可以叫tail、back和end,而队头也可以叫做head,在本文中,我们使用insert和delete来描述两个操作,队尾用rear,队头用front。
当队列为空时,队尾指向下标为-1的位置,队头front指向下标为0的位置
插入(insert)
当要插入一个新的数据时,会先将队尾的位置先加1,指向下一个位置,然后在该位置上放入新的数据。
移除(delete)
当要在队列中移除并且返回其值时,会先返回队头fron所指向的值,然后再将front值加1上移。
具体操作过程如下图:
队空队满
当想空的队列中移除或者访问数据时,就会报”can’t remove, queue is empty“,当想在已满的队列中插入新的数据时,会报”can’t insert, queue is full“。
循环队列
在上述的操作其实存在着一个问题,如果队尾已经移动到队顶,就不能再插入数据,但此时队列不一定是满的,或者队头已经移动到队顶,此时也不能移除数据,但队列也不一定是空的,这样的话由于使用的是数组实现的,不能扩大队列的长度,那么就只能重新创建一个更大一点队列,这回造成对内存空间的浪费。
那么该怎么处理这个问题呢?
环绕式处理
为了避免上述问题,我们可以在队头或者队尾移动到队顶时,将其重新赋值,是其绕回到下标为0的位置,这就是循环队列(有时也成为缓冲环)。
插入更多的数据项。这个时候当队尾的值为队列所能存贮的大小,而要再插入新的数据时,队尾会绕道下标为0的位置,再赋予新值,此时队尾rear处于队头front的下面,颠倒了初始的位置,这可以被称为”折断的队列“,队列中的数据被存放在数组的两个不同的序列中。然后如果再插入新的数据,队尾又会不断变大而向上移动。如下图:
删除更多的数据项。当队头值等于队列所能存贮的大小时,队头会绕到下标为0的位置,指定新的队头,下次再删除数据时,将删除此时队头所指的值。这个时候队列当中的数据又恢复到单一序列。
循环队列的Java代码
class Queue {
private int maxSize; //表示队列所能存放的大小
private int[] array;
private int nItem; //表示当前已经存放的数据个数
private int front;
private int rear;
public Queue(int max) {
maxSize = max;
array = new int[max];
nItem = 0;
front = 0;
rear = -1;
}
public void insert(int val) {
rear ++;
if(rear == maxSize)
rear = 0;
array[rear] = val;
nItem ++;
}
public int delete() {
int temp = array[front ++];
if(front == maxSize)
front = 0;
nItem --;
return temp;
}
public int peak() {
return array[front];
}
public boolean isEmpty() {
return (nItem == 0)
}
public boolean isFull() {
return (nItem == maxSize)
}
public int size() {
return nItem;
}
}//end Queue
在上述代码中,isEmpty()、isFull()和size()方法的实现等使用了记录队列中数据项个数的变量nItem。但是这种使用会使得程序产生额外的操作。
没有数据项计数字段的队列实现
nItem的使用在插入和移除操作不多时,并不会有多大的影响,但是当这两种操作较多时,可能就会影响程序的性能。
因此在某些队列的实现时,并不会使用nItem这个变量,而是利用对队头front和队尾rear的一些计算来代替nItem在程序中的作用。但是由于队尾的环绕,可能会出现折断的队列,所以对于是否为空,或者是否已满的判断会变得复杂。
这个问题是可以解决的,在下列代码中实现了这些操作,在这个代码中数组所能存放的个数maxSize要比队列数据项个数s要大一,即maxSize=s+1,那么在这个时候队头和队尾所能指向的最大下标也变为了maxSize-2,当等于maxSize-1时,就说明要环绕到下标为0的位置:
class Queue {
private int maxSize;
private int[] array;
private int front;
private int rear;
public Queue(int s) {
maxSize = s + 1;
array = new int[maxSize];
front = 0;
rear = -1;
}
public void insert(int val) {
rear ++;
if(rear == maxSize-1)
rear = 0;
array[rear] = val;
}
public int delete() {
int temp = array[front --];
if(front == maxSize-1)
front = 0;
return temp;
}
public int peak() {
return array[front];
}
public boolean isEmpty() {
return (rear == front);
}
public boolean isFull() {
return ((rear + 1) % (maxSize - 1) == front);
}
public int size() {
if(rear > front)
return (rear - front + 1);
else
return (maxSize - front) + (rear + 1);
}
}//end Queue
队列的效率
队列的插入和移除也与队列的数据个数无关,所以所要消耗的时间也为O(1)
双端队列
双端队列即在队列两端都可以进行插入和移除数据项的操作。这些方法被称为insertLeft和removeLeft,以及insertRight和removeRight。
如果严格禁止调用某一端的插入和移除,那么这个双端队列就和栈的功能相同。如果禁止调用insertLeft和removeRight,那么这就是一个普通队列。
双端队列集齐了队列和栈的功能,是一种多用途的数据结构,不过似乎也不怎么常用。