队列(Queue)
- 是一种线性结构
- 只能从队尾添加元素,称为入队(Enqueue);
- 只能从队首取出元素,称为出队(Dequeue);
先进先出的特性(FIFO-first in first out
):最先插入的元素最先出来.
back:当前队尾的索引(下面我们用tail代替)
front:当前队首的索引
普通数组队列的问题
取元素时只能从数组首端,也就是索引为0的位置取出
存放元素只能从数组末端添加,
而且队首到队尾之间的元素不能存在空闲,要一个紧挨着一个
但会存在一个缺陷,想象一下:
**如果从队首取出一个元素,那么索引为0的位置就空了出来,
必须把后继的所有元素遍历向前挪一个位置;**
这样操作时间复杂度为O(n):
n代表着数组中的元素个数,这个算法的运行时间与元素个数是一个线性的关系,也就是说nums中的元素个数越多,这个方法的运行时间就越长.
为了解决这个问题,需要抽象一下:
循环队列的机制:
front:当前队首的索引
tail:当前队尾的索引
capcity:容量,由用户定义数组可容纳多少元素
把整个数组想象成一个首尾相连的闭环,队首元素出队后不再挪动其他元素,
而是维护一个front的变量表示当前队首的位置;
当数组末端位置占满后,新入队的元素添加到数组前端空虚的位置,
维护一个tail的变量表示当前队尾的位置,tail = 当前末端索引+1 % arr.length;
但是注意当这个闭环要满的时候,最后一个位置也就是tail所指的位置不能再插入元素了,
防止front == tail,此时就应该扩容,所以获取容量的时候应该忽略这个空闲的位置capacity = arr.length -1
设计了以上措施后front == tail的情况只能是队列为空
(tail + 1) % arr.length == front 表示队列已满
如此,dequeue()的复杂度就成了O(1) (均摊)
下面是具体代码,包含扩容机制和时间复杂度测试:
定义接口,使用泛型,让它可以存储任意类型的数据.
public interface Queue<E> {
//获取有效的元素个数
int getSize();
//查看队列是否为空
boolean isEmpty();
//入队
void enqueue(E e);
//出队
E dequeue();
//查看队首的元素
E getFront();
}
实现类:
public class LoopQueue<E> implements Queue<E> {
//队列元素
private E[] data;
//队首, 队尾的位置
private int front, tail;
private int size;
//根据用户指定的容量初始化队列
public LoopQueue(int capacity){
//需要预留出一个空间
data = (E[])new Object[capacity + 1];
//初始化成员变量
front = 0;
tail = 0;
size = 0;
}
//无参构造
public LoopQueue(){
//调用有参构造,设置默认容量为10
this(10);
}
//获取队列的容量
public int getCapacity(){
//在数组中有一个空间始终会被空闲
return data.length - 1;
}
//获取有效元素的个数
@Override
public int getSize() {
return size;
}
//查看队列是否为空
@Override
public boolean isEmpty() {
return front == tail;
}
//向队列尾部添加元素(入队)
@Override
public void enqueue(E e) {
//判断队列是否已满
if ((tail + 1) % data.length == front)
//扩容为当前容量的2倍
resize(getCapacity() * 2);
data[tail] = e;
//维护tail值
tail = (tail + 1) % data.length;
size ++;
}
private void resize(int newCapacity){
//仍然需要预留一个空间
E[] newData = (E[]) new Object[newCapacity + 1];
//将元素复制到新的数组中
for (int i = 0; i < size; i++)
// data中的元素相对于newData中的元素,
// 索引值是存在front这么多的偏移的(没懂)
// % data.length是为了防止越界
newData[i] = data[(i + front) % data.length];
data = newData;//data指向新的数组
front = 0;//队首又回到了0号索引
tail = size;//对尾便是size
}
//队首元素出队操作
@Override
public E dequeue() {
//如果队列是空的,抛出异常
if (isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
//保存出队的元素
E returnElement = data[front];
//将它的引用变为null,垃圾回收器就会回收这片空间
data[front] = null;
//维护front值
front = (front + 1) % data.length;
size --;
// 如果当前数组的容量只有1/4被利用到,就进行缩容
// Lazy的思想,延迟缩容,防止复杂度震荡
if (size == getCapacity() / 4 && getCapacity() / 2 != 0)
resize(getCapacity() / 2);
return returnElement;
}
//查看队首的元素
@Override
public E getFront() {
//如果队列是空的,抛出异常
if (isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
//返回front位置的元素即可
return data[front];
}
@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) {
LoopQueue<Integer> queue = new LoopQueue<>();
for (int i = 0; i < 10; i++){
//将0~9的数字入队
queue.enqueue(i);
System.out.println(queue);
//每隔3个数字,做一次出队操作
if (i % 3 == 2){
queue.dequeue();
System.out.println(queue);
}
}
}
--------console---------
Queue: size = 1 , capacity = 10
front [0] tail
Queue: size = 2 , capacity = 10
front [0, 1] tail
Queue: size = 3 , capacity = 10 //触发缩容
front [0, 1, 2] tail
Queue: size = 2 , capacity = 5
front [1, 2] tail
Queue: size = 3 , capacity = 5
front [1, 2, 3] tail
Queue: size = 4 , capacity = 5
front [1, 2, 3, 4] tail
Queue: size = 5 , capacity = 5
front [1, 2, 3, 4, 5] tail
Queue: size = 4 , capacity = 5
front [2, 3, 4, 5] tail
Queue: size = 5 , capacity = 5
front [2, 3, 4, 5, 6] tail
Queue: size = 6 , capacity = 10 //触发扩容
front [2, 3, 4, 5, 6, 7] tail
Queue: size = 7 , capacity = 10
front [2, 3, 4, 5, 6, 7, 8] tail
Queue: size = 6 , capacity = 10
front [3, 4, 5, 6, 7, 8] tail
Queue: size = 7 , capacity = 10
front [3, 4, 5, 6, 7, 8, 9] tail
与普通数组队列的执行时间对比:
public class Main {
//测试使用q运行opCount个enqueue和dequeue操作所需要的时间,单位:秒
private static double testQueue(Queue<Integer> q,int opCount){
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i < opCount; i++)
//生成0 ~ Integer最大值之间的随机数入队
q.enqueue(random.nextInt(Integer.MAX_VALUE));
for (int i = 0; i < opCount; i++)
q.dequeue();
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0;
}
public static void main(String[] args) {
int opCount = 100000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
double time1 = testQueue(arrayQueue,opCount);
System.out.println("ArrayQueue, time:" + time1 + " s");
LoopQueue<Integer> loopQueue = new LoopQueue<>();
double time2 = testQueue(loopQueue, opCount);
System.out.println("LoopQueue, time:" + time2 + " s");
}
}
-----------console------------
ArrayQueue, time:3.306405 s
LoopQueue, time:0.0124581 s //优化了太多妙啊
结合自己的理解记录了学习的过程,希望对你有帮助
图片来自网络
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。