(一) 队列
1.定义
队列也是一种操作受限的线性表数据结构, 它只允许在表的前端(队首)进行删除操作,而在表的后端(队尾)进行插入操作. 队列是一种先进先出的数据结构(First In First Out: FIFO). 相比较数组, 栈对应的操作是数组的子集.
2.图解
- 队列的存储结构: front队首指针等于tail队尾指针, 队列为空.
- 队列依次入队添加元素 A、B、C, 每次入队 tail++
- 队列出队移除元素, front++
- 假溢出: 在顺序队列中,当队尾指针已经到数组的上界,不能再有入队操作,否则会造成数组越界而遭致程序出错, 也不能扩容, 因为数组中还有大量空间未被占用。
- 循环队列: 解决顺序队列假溢出问题, 循环队列仍然是基于数组实现.
循环队列中的front指针或tail指针指向队尾时,只要队首还存在未使用的空间, 那么指针可以循环到队首0索引处. 但此时,
front == tail 既可以表示队列为空, 又可以表示队列已满.
解决方案:
- 设标志位法: 设置一个标志量flag: 入队操作设置flag=1, 出队操作设置flag=0.
当front == tail && flag == 1, 队列已满; 当front == tail && flag == 0, 队列为空. - 预存长度法: 设置一个计数器size记录队列中元素个数.
- 空闲单元法: 人为浪费一个单元, 如上图所示. 当 front == tail, 队列为空; 当front == (tail + 1) % size, 队列已满.
(size 为数组长度, 取余操作时保证队尾指针tail在数组长度内), 下文的LoopQueue循环队列采用此方法实现.
(二) 队列的基本实现
基于<<数据结构与算法之数组>>中的动态数组实现
1.新建接口Queue
/**
* 自定队列数据结构接口
*
* @author Administrator
*
*/
public interface Queue<E> {
/**
* 入队: 向队列(队尾)中添加元素
*
* @param e
*/
void enqueue(E e);
/**
* 出队: 从队列(队首)中取出一个元素
*
* @return
*/
E dequeue();
/**
* 查看队首元素
*
* @return
*/
E getFront();
/**
* 获取队列中有效元素的个数
*
* @return
*/
int getSize();
/**
* 返回队列中有效元素是否为空
*
* @return
*/
boolean isEmpty();
}
2.自定义ArrayQueue数组队列
/**
* 自定义数组(顺序)队列实现Queue接口
*
* @author Administrator
*
* @param <E>
*/
public class ArrayQueue<E> implements Queue<E> {
private Array<E> array;
public ArrayQueue() {
array = new Array<>();
}
public ArrayQueue(int capacity) {
array = new Array<>(capacity);
}
/**
* 入队: 向队列(队尾)添加一个元素
*/
@Override
public void enqueue(E e) {
array.addLast(e);
}
/**
* 出队: 从队列(队首)取出一个元素
*/
@Override
public E dequeue() {
return array.removeFirst();
}
/**
* 查看队首元素
*/
@Override
public E getFront() {
return array.getFirst();
}
/**
* 查看队列中有效元素的个数
*/
@Override
public int getSize() {
return array.getSize();
}
/**
* 返回队列中有效元素是否为空
*/
@Override
public boolean isEmpty() {
return array.isEmpty();
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Queue: ");
res.append("front [");
for (int i = 0; i < array.getSize(); i++) {
res.append(array.get(i));
if (i != array.getSize() - 1) {
res.append(", ");
}
}
res.append("] tail");
return res.toString();
}
}
测试
public static void main(String[] args) {
ArrayQueue<Integer> queue = new ArrayQueue<>();
// 向队列中添加元素: 0-9
for (int i = 0; i < 10; i++) {
queue.enqueue(i);
System.out.println(queue);
// 每添加三个元素, 移除队列队首元素
if(i % 3 == 2) {
queue.dequeue();
System.out.println(queue);
}
}
}
front: 代表队列队首; tail: 代表队列队尾.
数组队列的时间复杂度分析
函数 | 时间复杂度 | 分析 |
---|---|---|
enqueue(e) | O(1) | 直接往size索引处赋值,此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
dequeue() | O(n) | 从队列(队首)中取出一个元素, 所有元素向后移一个单位, 与数据规模呈线性关系 |
getFront() | O(1) | 直接获取0索引处的值,此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
getSize() | O(1) | 直接获取size的值,此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
isEmpty() | O(1) | 直接判断size==0,此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
数组队列的dequeue()操作, 从队列(队首)中取出一个元素, 所有元素向后移一个单位. 它的时间复杂度是 O(n) 级别的. 当数组队列中数据量很大情况下, 是稍微不合理的. 那么dequeue()操作的时间复杂度有没有可能是 O(1) 级别的?
循环队列 的出现很好的解决了这个问题.
3.自定义LoopQueue循环队列
- 创建LoopQueue类实现Queue接口
public class LoopQueue<E> implements Queue<E> {
/**
* 存储数据的源数组
*/
private E[] data;
/**
* 队首指针
*/
private int front;
/**
* 队尾指针
*/
private int tail;
/**
* 无参数的构造函数, 默认数组的容量capacity=10
*/
public LoopQueue() {
this(10);
}
/**
* 构造函数, 创建capacity + 1容量的数组. 我们采用了牺牲一个元素空间,来区别队空或队满的方式,
* 但用户使用LoopQueue类, 无需关注底层实现, 为给用户带来更好的体验, 因此我们补全了一个单位的空间
*
* @param capacity
*/
@SuppressWarnings("unchecked")
public LoopQueue(int capacity) {
data = (E[]) new Object[capacity + 1];
front = 0;
tail = 0;
}
@Override
public void enqueue(E e) {
}
@Override
public E dequeue() {
return null;
}
@Override
public E getFront() {
return null;
}
@Override
public int getSize() {
return 0;
}
@Override
public boolean isEmpty() {
return false;
}
}
- 完善isEmpty()方法: 返回队列中有效元素是否为空
/**
* 返回队列中有效元素是否为空: 队尾指针数 等于 队首指针数
*/
@Override
public boolean isEmpty() {
return tail == front;
}
- 完善getSize()方法: 获取队列中有效元素的个数
/**
* 获取队列中有效元素的个数: 分两种情况
* 1. front队首指针数 小于 tail队尾指针数, 相当于数组队列, 有效元素的个数 = 队尾指针数 - 队首指针数
* 2. front队首指针数 大于 tail队尾指针数, 循环队列, 有效元素的个数 = 队尾指针数 + 队列容量与队首指针数之差
*/
@Override
public int getSize() {
return front > tail ? tail + (data.length - front) : tail - front;
}
- 完善getFront()方法: 查看队首元素
@Override
public E getFront() {
return data[front];
}
- 新增getCapacity(): 获取循环队列的真实容量
/**
* 获取循环队列的真实容量: 当前数组长度-补全的一个单位的长度
*
* @return
*/
@Override
public int getCapacity() {
return data.length - 1;
}
- 新增getCapacity(): 获取循环队列的真实容量
/**
* 获取循环队列的真实容量: 当前数组长度-补全的一个单位的长度
*
* @return
*/
@Override
public int getCapacity() {
return data.length - 1;
}
- 新增resize(capatity): 重置队列容量
/**
*重置队列容量, 新建一个容量为newCapacity的队列赋值给data
*
* @param capaccity
*/
@SuppressWarnings("unchecked")
private void resize(int capacity) {
E[] newData = (E[]) new Object[capacity + 1];
// 获取原队列有效元素个数
int size = getSize();
// 遍历循环队列, 赋值给新队列
for (int i = 0; i < size; i++) {
newData[i] = data[(i + front) % data.length];
}
data = newData;
// 队首指针指向新队列0索引处
front = 0;
// 队尾指针指向 原队列有效元素个数 位置处
tail = size;
}
- 完善enqueue(e)方法: 入队, 向循环队列添加一个元素
@Override
public void enqueue(E e) {
/*
* front == (tail + 1) % data.length: 代表循环队列已满, 扩容原容量的2倍
*/
if (front == (tail + 1) % data.length) {
resize(2 * getCapacity());
}
data[tail] = e;
// 取余操作: 保证队尾指针tail在数组长度内, 而不是简单队尾指针向后移一个单位
tail = (tail + 1) % data.length;
}
- 完善dequeue()方法: 出队, 从队列(队首)取出一个元素
@Override
public E dequeue() {
if (isEmpty()) {
throw new IllegalArgumentException("LoopQueue is empty.");
}
E e = data[front];
data[front] = null;
// 取余操作: 保证队首指针front在数组长度内, 而不是简单队首指针向后移一个单位
front = (front + 1) % data.length;
// 当队列的有效元素等于队列容量的四分之一时, 队列缩容至原容量的二分之一, 防止复杂度震荡
if (getSize() == getCapacity() / 4 && getCapacity() / 2 != 0) {
resize(getCapacity() / 2);
}
return e;
}
测试
public static void main(String[] args) {
LoopQueue<Integer> queue = new LoopQueue<>();
// 向队列中添加元素: 0-9
for (int i = 0; i < 10; i++) {
queue.enqueue(i);
System.out.println(queue);
// 每添加三个元素, 移除队列队首元素
if(i % 3 == 2) {
queue.dequeue();
System.out.println(queue);
}
}
}
函数 | 时间复杂度 | 分析 |
---|---|---|
dequeue() | O(1) | 从循环队列中取出一个元素, 只需 (front+1)%data.length 操作, 此操作消耗的时间与数据规模无关系的, 在常数时间内完成 |
- 数组队列和循环队列的比较
public static void main(String[] args) {
int opCount = 200000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
LoopQueue<Integer> loopQueue = new LoopQueue<>();
System.out.println("ArrayQueue, time: " + testQueue(arrayQueue, opCount) + " s");
System.out.println("LoopQueue, time: " + testQueue(loopQueue, opCount) + " s");
}
/**
* 测试使用queue运行opCount个enqueue和dequeue操作所需要的的时间, 单位: 秒
*
* @param queue
* @param opCount
* @return
*/
public static double testQueue(Queue<Integer> queue, int opCount) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < opCount; i++) {
queue.enqueue(i);
}
for (int i = 0; i < opCount; i++) {
queue.dequeue();
}
long endTime = System.currentTimeMillis();
return (endTime - startTime) / 1000.0;
}