队列
我在这一章的前半部分向大家介绍了栈这种数据结构,那么我在这章的后部分,向大家介绍队列这种数据结构。
队列的概括
队列,本身也是一种线性数据结构,换句话说数据依旧是排成一排,这一点和栈一样。不过和栈不同的是,队列只能从一端添加元素,而从另一端取出元素v,通常。我们添加元素的一端称为队尾,而取出元素的一端称为队首**。事实上这一切都非常好理解,队列这个名字的由来也和我们生活中,排队那个队列是一致的。
我们简单的看一个如图演示:
假设这就是一个队列,可以想象一下,比如说
- 我们去银行办事情,相应的就有一个柜台,队列就是排队到这个柜台办业务
- 那么一旦来了一个新的人。在数据结构领域,其实就是来了一个新的元素,这个新的元素就应该从队尾的位置进入队列
- 下面再来一个新的元素,这个新的元素依然是从队尾的位置进入队列,以此类推……
- 但是现在要出队了,比如说,在柜台的服务人员已经办完了上一个业务,已经有空闲处理下一个任务啦,那么此时,就要队列中取出一个元素,取出的元素就应该是队首元素,这种情况下取出的元素就应该是1。
通过分析可以看出,队列是一种先进先出的数据结构,也就是生活中的先到先得,这是和栈的后进先出不一样,这个区别(从每端取出数据的区别)看似很小的。但是,在实际的应用中却大不相同。如果我们在生活中用栈这种方式来进行排队办理业务的话,相信没有人会愿意的。相应的这种先进先出,英文就是first in first out,缩写成FIFO
队列的实现
针对队列这种数据结构来说,基本上写了只涉及这5个操作,分别是
- 向队列中添加一个元素,也都是入队,通常叫做
enqueue
- 从队列中取出一个元素,也叫做出队,通常叫做
dequeue
- 看一下队首列的元素是谁,这个动作通常叫做
getFront
- 看一下队列里一共有多少个元素
getSize
- 判断一下队列是否为空
isEmpty
我们实现的方式也是写一个接口叫做queue,我们依托不同的底层的数据结构可以实现这个接口。在队列的实现中,我将非常快速的和之前的栈一样,复用Array这个动态数组类来实现ArrayQueue
需要提一下,从用户的角度来看,只要实现上述操作就好,具体底层实现,用户并不关心,实际上,底层确实有多种实现方式。
我们准备在之前实现的动态数组基础上,来实现"队列"这种数据结构。
先定义一个接口Interface,如下:
|
ArrayQueue实现代码如下:
package com.lihe.leetcode;
public class ArrayQueue<E> implements Queue<E> {
private Array<E> array;
//构造函数
public ArrayQueue(int capacity) {
array = new Array<>(capacity);
}
//无参构造函数
public ArrayQueue(){
array = new Array<>();
}
@Override
public int getSize() {
return array.getSize();
}
@Override
public boolean isEmpty() {
return array.isEmpty();
}
@Override
public void enqueue(E e) {
array.addLast(e);
}
@Override
public E dequeue() {
return array.removeFirst();
}
@Override
public E getFront() {
return array.getFirst();
}
public int getCapacity(){
return array.getCapacity();
}
@Override
public String toString(){
StringBuilder ret = new StringBuilder();
ret.append("Queue:");
ret.append("Front [");
for (int i = 0; i < array.getSize(); i++) {
ret.append(array.get(i));
if(i != array.getSize() - 1)
ret.append(", ");
}
ret.append("] tail");
return ret.toString();
}
public static void main(String[] args) {
ArrayQueue<Integer> queue = new ArrayQueue<>();
for (int i = 0; i < 10; i++) {
queue.enqueue(i);
System.out.println(queue);
if(i % 3 == 2){
queue.dequeue();
System.out.println(queue);
}
}
}
/** 结果:
* Queue:Front [0] tail
* Queue:Front [0, 1] tail
* Queue:Front [0, 1, 2] tail
* Queue:Front [1, 2] tail
* Queue:Front [1, 2, 3] tail
* Queue:Front [1, 2, 3, 4] tail
* Queue:Front [1, 2, 3, 4, 5] tail
* Queue:Front [2, 3, 4, 5] tail
* Queue:Front [2, 3, 4, 5, 6] tail
* Queue:Front [2, 3, 4, 5, 6, 7] tail
* Queue:Front [2, 3, 4, 5, 6, 7, 8] tail
* Queue:Front [3, 4, 5, 6, 7, 8] tail
* Queue:Front [3, 4, 5, 6, 7, 8, 9] tail
*/
}
队列的复杂度分析
最后,对于我们实现的ArrayQueue数组队列,进行一下简单的复杂度分析,这个复杂度分析大部分和之前的栈一样。
在添加元素(入队enqueue)的时候,每一次都是向数组的最后一个位置添加元素,在对进行均摊复杂度分析,最终是O(1)的复杂度,同时getSize
,isEmpty
,getFront
也都是O(1)的复杂度。不过,我们出队(dequeue)操作是一个O(n)复杂度的操作。这是因为在出队的时候,需要把整个数组的第一个元素(索引为0)拿出来,而数组中后面剩下的所有元素都要向前挪一个位置,这显然是O(n)的复杂度。
ArrayQueue<E>
·void enqueue(E) O(1) 均摊
·E dequeue() O(n)
·E getFront() O(1)
·int getSize() O(1)
·boolean isEmpty() O(1)
循环队列
- 数组队列的出队操作的复杂度是O(n),性能很差,解决方法就是使用循环队列(Loop Queue)
- 正常情况:front指向队列第一个元素,tail指向队列中最后一个元素的后一个位置
- 队列为空时,front和tail都指向第一个元素位置,不为空时,front != tail
- 入队时:tail ++, 出队时: front ++
- 循环队列的示意图如下:
入队出队过程:入队5个元素
出队两个元素(只需要front指向改变,不需要移动所有元素,复杂度为O(1)):
入队4个元素
注意:因为front == tail表示队列为空,故此时如果在添加一个元素会导致front == tail,然而此时表示队列已满。
因此,当tail + 1 == front 时表示队列为满。因为是循环队列故使用(tail + 1)% c == front(当front=0,tail=7时,使用(7+1)%8 ==0)。c代表capacity(容积)。
结论:循环队列使用%,类似钟表11点之后叫12点或者0点
实现循环队列的业务逻辑,并进行测试:
-
package com.lihe.datastructure; public class LoopQueue<E> implements Queue<E>{ private E[] data; //队列 private int front,tail; //队列队首队尾 private int size; //队列元素个数 //有参构造函数 public LoopQueue(int capacity) { //因为循环队列浪费一个空间,需要存储capacity个元素,故需要capacity+1的空间 data = (E[])new Object[capacity + 1]; front = 0; tail = 0; size = 0; } //无参构造函数 public LoopQueue() { this(10);//直接调用有参数的构造函数,然后传入一个默认值 } //实际存储数据为容积-1,有意浪费一个空间 //容积比长度少一 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){ //扩容成所能存放个数的两倍,不是空间的两倍,因为浪费了一个空间 resize(getCapacity() * 2); } data[tail] = e; 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中的元素不一定从0开始,与newData之间偏移量front, //因是循环队列防止越界,故用%data.length newData[i] = data[(front + i) % data.length]; } data = newData; front = 0; tail = size;//元素个数不变 } //出队 @Override public E dequeue() { if(isEmpty()){ throw new IllegalArgumentException("Cannot enqueue from an empty queue"); } E ret = data[front]; front = (front + 1) % data.length; size --; if(size == getCapacity() / 4 && getCapacity() / 2 != 0) resize(getCapacity() / 2); return ret; } @Override public E getFront() { if(isEmpty()){ throw new IllegalArgumentException("Queue is empty "); } return data[front]; } //override代表复用 @Override public String toString() { StringBuilder res = new StringBuilder(); res.append(String.format("Queue:size=%d,capacity=%d\n", size, getCapacity())); res.append("front ["); //遍历循环队列,队首是front,队尾是tail-1, // 因为是循环故tail可能<front,故条件为i != tail不是i < tail, // 故递增为 i= (i + 1) % data.length 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++) { queue.enqueue(i); System.out.println(queue); if(i % 3 == 2){ queue.dequeue(); System.out.println(queue); } } } } /** * 遍历循环队列中两种方法: * 法一: * for (int i = 0; i < size; i++) { * //data中的元素不一定从0开始,与newData之间偏移量front, * //因是循环队列防止越界,故用%data.length * newData[i] = data[(front + i) % data.length]; * } * * 法二: * //遍历循环队列,队首是front,队尾是tail-1, * // 因为是循环故tail可能<front,故条件为i != tail不是i < tail, * // 故递增为 i= (i + 1) % data.length * for (int i = front; i != tail; i= (i + 1) % data.length ) { * res.append(data[i]); * //不是最后一个元素的判断条件 * if ((i + 1) % data.length != tail) { * res.append(','); * } * } */
- 输出结果:
-
/** * 结果: * 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 */
循环队列的复杂度分析
-
LoopQueue<E> ·void enqueue(E) O(1) 均摊 ·E dequeue() O(1) 均摊 ·E getFront() O(1) ·int getSize() O(1) ·boolean isEmpty() O(1)
使用简单算例测试ArrayQueue与LoopQueue的性能差异
package com.lihe.datastructure;
import java.util.Random;
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++) {
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");
}
}
- 输出结果
-
ArrayQueue, time: 5.891407568 s LoopQueue, time: 0.018795095 s