本文来源于liuyubobobo的“玩转数据结构 从入门到进阶”视频教程
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。
假设我们使用数组实现队列,请看下图
入队操作很快速,只需要在队尾添加元素即可。但是,出队操作就耗时了,把队头元素删除并返回给调用者后,还需要把数组中的所有元素往前移动,这一步非常耗时。
a出队后,b、c、d要移动到下标为0、1、2的位置
有办法做到出队时,数组元素不移动吗?有滴,使用循环数组
循环队列有几个特点:
1、front变量指向队头(有元素),tail变量指向队尾(无元素)。
2、出队时front往后移动一个下标(“往后”这种说法不是完全正确,后面再给出精确定义;扩容后面会讲)
3、入队则是在队尾添加元素,队尾往后移动一个下标
4、front==tail表示队列为空,所以队列满的时候,不是front、tail都指向同一个元素,而是tail在front的前一个位置。
上图是队列满的情况,数组的长度是8,但队列只能储存7个元素,必须让一个元素为空,tail指向这个元素。
是想下,如果非要用完数组的所有存储空间,此时front==tail;而队列为空是也是front==tail,在代码实现上要区分何时队列为空、何时队列满就复杂了。干脆就“浪费”一个元素内存,front==tail就只有队列为空这一种情况。
5、定义数组长度data.length(图中数组长度是8),队列满的时候判断条件是 (tail+1) % data.length == front ,大家照着图好好理解这个判断条件。
6、入队,tail移动方式是 tail = (tail + 1) % data.length
出队,front移动方式是 front = (front + 1) % data.length
用上图举个例子
入队操作,tail = (1+1) % 8 = 2,tail变成2
出队操作,front = (3+1) % 8 = 4,front变成4
当front在7位置,出队,fornt = (7+1) % 8 = 0,此时front指向下标0的位置
7、扩容,如下图。满足条件(tail + 1) % data.length == front,此时需要扩容
实现代码如下
// 基于循环数组实现队列
public class LoopQueue<E>{
private E[] data; // 数组
private int front, tail; //队头、队尾
private int size; //队列元素大小
public LoopQueue(){
data = (E[]) new Object[11]; //初始化队列的数组
// 初始化时队头、队尾下标都为0,队列大小也是0
front = 0;
tail = 0;
size = 0;
}
// 队列容量是数组长度减1,因为要空一个元素,以便区别队列是否为空、队列是否满了需要扩容
public int getCapacity(){
return data.length -1;
}
//队列为空
public boolean isEmpty(){
return front == tail;
}
//队列大小
public int getSize(){
return size;
}
//扩容、缩容
private void resize(int newCapacity){
// 数组长度要比容量多一个
E[] newData = (E[])new Object[newCapacity+1];
//将旧数据从队头开始,添加到新数组中
for (int i=0; i<size; i++){
newData[i] = data[(i+front) % data.length];
}
front = 0; //添加完成,队头在新数组下标为0的位置
tail = size; //添加完成,队列大小就是队尾的下标
data = newData; //队列的数组data指向扩容后的数组
}
//入队
public void enqueue(E e){
//如果队列满了,则扩容
if ((tail + 1) % data.length == front){
resize(getCapacity() * 2);
}
data[tail] = e;
//队尾下标移动
tail = (tail + 1) % data.length;
//队列大小加一
size++;
}
//出队
public E dequeue(){
if (isEmpty())
throw new RuntimeException("没数据了");
//队头数据存储到变量中
E ret = data[front];
data[front] = null;
//队头下标移动
front = (front + 1) % data.length;
//队列大小减一
size--;
//这是缩容,如果队列大小小于容量的1/4且容量的1/2不为0,队列缩容为原来的1/2
if (size < getCapacity()/4 && getCapacity()/2 != 0){
resize(getCapacity()/2);
}
//返回队头数据
return ret;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append(String.format("Queue: size = %d , capacity = %d\n", size, getCapacity()));
res.append("front [");
for(int i = front ; i != tail ; i = (i + 1) % data.length){
res.append(data[i]);
if((i + 1) % data.length != tail)
res.append(", ");
}
res.append("] tail");
return res.toString();
}
public static void main(String[] args){
_2_LoopQueue.LoopQueue<Integer> queue = new _2_LoopQueue.LoopQueue<>();
for(int i = 0 ; i < 20 ; i ++){
queue.enqueue(i);
System.out.println(queue);
if(i % 10 == 0){
queue.dequeue();
System.out.println(queue);
}
}
for(int i = 0 ; i < 18 ; i ++){
queue.dequeue();
System.out.println(queue);
}
}
}