1、概述
队列(Queue),线性数据结构。
队列这一数据结构来自于现实,现实中的“排队”抽象出来就是队列。
向队列中添加元素称为“入队”,从队列中移出元素称为“出队”。
队列的特点是“一端进,另一端出”并且“先进先出”。“入队”的一端称为“队尾”,“出队”的一端称为“队首”。
队列-动画演示
2、基于 Java 编写一个队列的接口
与栈一样,队列也属于一种规则性的数据结构,同样用接口编写队列的规则。
public interface Queue {
/**
* 入队
*
* @param e 入队的元素
*/
void enqueue(E e);
/**
* 出队
*
* @return 返回出队的元素
*/
E dequeue();
/**
* 获取队首元素
*
* @return 队首元素
*/
E getHead();
/**
* 是否是空队列
*
* @return 返回是否是空队列
*/
boolean isEmpty();
/**
* 获取队列中元素个数
*
* @return 返回队列中元素个数
*/
int getSize();
}
3、基于 Java 动态数组实现队列
import java.util.Objects;
/**
* 自定义动态数组
*/
public class Array {
/**
* 封装的原生数组
*/
private E[] data;
/**
* 数组中元素个数
*/
private int size;
/**
* 数组开辟的空间大小
*/
private int capacity;
/**
* 默认开辟空间大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 默认扩容倍数
*/
private static final int DEFAULT_INCREASE_CAPACITY_TIMES = 2;
/**
* 默认缩容倍数
*/
private static final int DEFAULT_DECREASE_CAPACITY_TIMES = 2;
/**
* 缩容时机
*
* 当元素个数仅仅是开辟空间大小的 1/4 时
*/
private static final int DEFAULT_DECREASE_CAPACITY_WHEN = 4;
/**
* 在数组中未发现元素时的返回值
*/
private static final int NOT_FIND_INDEX_FLAG = -1;
public Array() {
data = (E[]) new Object[DEFAULT_CAPACITY];
capacity = DEFAULT_CAPACITY;
}
public Array(int capacity) {
if (capacity > 0) {
data = (E[]) new Object[capacity];
} else {
throw new IllegalArgumentException("Capacity Error!");
}
}
/**
* 在指定索引处增加元素
*
* @param e e
* @param index index
*/
public void addElement(E e, int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Index Error!");
}
if (size == capacity) {
reCapacity(capacity * DEFAULT_INCREASE_CAPACITY_TIMES);
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
/**
* 在数组首部增加元素
*
* @param e e
*/
public void addElementAtFirst(E e) {
addElement(e, 0);
}
/**
* 在数组尾部增加元素
*
* @param e e
*/
public void addElementAtLast(E e) {
addElement(e, size);
}
/**
* 删除指定索引的元素
*
* @param index index
* @return e
*/
public E removeElement(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Index Error!");
}
E oldElem = data[index];
for (int i = index; i + 1 < size; i++) {
data[i] = data[i + 1];
}
data[size - 1] = null;
size--;
if (size == capacity / DEFAULT_DECREASE_CAPACITY_WHEN) {
reCapacity(capacity / DEFAULT_DECREASE_CAPACITY_TIMES);
}
return oldElem;
}
public E removeElementAtFirst() {
return removeElement(0);
}
public E removeElementAtLast() {
return removeElement(size - 1);
}
/**
* 删除特定的元素,从数组首部开始检索
*
* @param e e
*/
public void removeSpecificElementFromFirst(E e) {
int index = findSpecificElementFromFirst(e);
if (index != NOT_FIND_INDEX_FLAG) {
removeElement(index);
}
}
/**
* 删除特定的元素,从数组尾部开始检索
*
* @param e e
*/
public void removeSpecificElementFromLast(E e) {
int index = findSpecificElementFromLast(e);
if (index != NOT_FIND_INDEX_FLAG) {
removeElement(index);
}
}
/**
* 删除特定的所有元素
*
* @param e e
*/
public void removeAllSpecificElements(E e) {
for (int i = 0; i < size; i++) {
if (Objects.equals(e, data[i])) {
removeElement(i);
}
}
}
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Index Error!");
}
return data[index];
}
public void set(E e, int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Index Error!");
}
data[index] = e;
}
public int findSpecificElementFromFirst(E e) {
for (int i = 0; i < size; i++) {
if (Objects.equals(e, data[i])) {
return i;
}
}
return NOT_FIND_INDEX_FLAG;
}
public int findSpecificElementFromLast(E e) {
for (int i = size - 1; i >= 0; i--) {
if (Objects.equals(e, data[i])) {
return i;
}
}
return NOT_FIND_INDEX_FLAG;
}
public E modifyElement(E e, int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Index Error!");
}
E oldElem = data[index];
data[index] = e;
return oldElem;
}
public boolean isEmpty() {
return size == 0;
}
public boolean contains(E e) {
for (int i = 0; i < size; i++) {
if (Objects.equals(e, data[i])) {
return true;
}
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < size; i++) {
sb.append(data[i]);
if (i != size - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
private void reCapacity(int newCapacity) {
E[] arr = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
arr[i] = data[i];
}
capacity = newCapacity;
data = arr;
}
public int getCapacity() {
return capacity;
}
public int getSize() {
return size;
}
}
/**
* 基于 动态数组 实现 队列
*
* 索引小的作为队首(左侧),索引大的作为队尾(右侧)
*
* @param
*/
public class ArrayQueue implements Queue {
private Array arr;
public ArrayQueue() {
arr = new Array<>();
}
public ArrayQueue(int capacity) {
arr = new Array<>(capacity);
}
@Override
public void enqueue(E e) {
arr.addElementAtLast(e);
}
@Override
public E dequeue() {
return arr.removeElementAtFirst();
}
@Override
public E getHead() {
return arr.get(0);
}
@Override
public boolean isEmpty() {
return arr.isEmpty();
}
@Override
public int getSize() {
return arr.getSize();
}
public int getCapacity() {
return arr.getCapacity();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("head [");
for (int i = 0; i < arr.getSize(); i++) {
sb.append(arr.get(i));
if (i != arr.getSize() - 1) {
sb.append(", ");
}
}
sb.append("] tail");
return sb.toString();
}
}
public class Main {
public static void main(String[] args) {
Queue queue = new ArrayQueue<>();
queue.enqueue(1);
queue.enqueue(3);
queue.enqueue(5);
queue.enqueue(7);
queue.enqueue(9);
System.out.println(queue);
queue.dequeue();
System.out.println(queue);
queue.dequeue();
System.out.println(queue);
queue.dequeue();
System.out.println(queue);
}
}
head [1, 3, 5, 7, 9] tail
head [3, 5, 7, 9] tail
head [5, 7, 9] tail
head [7, 9] tail
上述实现有一个小的问题,那就是在出队的时间复杂度上,入队的时间复杂度是
,但是出队的时间复杂度是
,原因是涉及到出队操作完成后,后续元素向前移动。所以,就有没有什么方案来将出队的时间复杂度也变为
,于是,便有了“循环队列”。
4、循环队列
循环队列的目的,就是为了优化“出队”这一操作的时间效率,“出队”操作后不需要元素移动,使之时间复杂度变为
。
循环队列的底层是数组。循环队列是对基于数组实现的队列的优化(基于链表实现的队列没有“循环队列”这一说法)。
循环队列的思想是,改变固有的思维,“数组是直的”变为“数组是一个首尾相接的圆环”。通过维护一些变量来记录“队首”、“队尾”,也就是说,“索引0一定是队首”的情况不再存在。
循环队列-模型
4.1、在不考虑扩容的情况下基于数组实现循环队列
动画演示:
循环队列-动画演示
常规循环队列中的一些约定:
维护着队首索引的变量为headIndex,维护着队尾索引的变量为tailIndex(注意:tailIndex是下一个入队元素的索引,headIndex是当前队首元素的索引)
1、为了使“队列为空”与“队列为满”这两种情况进行区分,真实数组的长度(arr.length)要比队列容量(capacity)多一个空间,否则的话,两种情况的判断标准就是一样的了。(如果使用变量对队列中元素个数进行维护的话,这个“多一个空间”不是必须的!!!)
2、队列为空:headIndex == tailIndex(两个变量不一定等于0)
3、队列为满:headIndex == (tailIndex + 1) % arr.length
4、入队操作后,tailIndex的维护:tailIndex = (tailIndex + 1) % arr.length
5、出队操作后,headIndex的维护:headIndex = (headIndex + 1) % arr.length
代码一:通过维护一个size变量来记录队列中的元素个数。
public interface Queue {
/**
* 入队
*
* @param e 入队的元素
*/
void enqueue(E e);
/**
* 出队
*
* @return 返回出队的元素
*/
E dequeue();
/**
* 获取队首元素
*
* @return 队首元素
*/
E getHead();
/**
* 是否是空队列
*
* @return 返回是否是空队列
*/
boolean isEmpty();
/**
* 获取队列中元素个数
*
* @return 返回队列中元素个数
*/
int getSize();
}
/**
* 循环队列
*/
public class LoopQueue implements Queue {
/**
* 数组
*/
private E[] arr;
/**
* 队列的最大容量
*/
private int capacity;
/**
* 队列中当前元素数目
*
* size 可以通过其他的方式计算得到
*/
private int size;
/**
* headIndex 队首索引
* tailIndex 队尾索引
*/
private int headIndex, tailIndex;
/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
public LoopQueue() {
// 根据循环队列的约定,数组的长度比队列最大容量要多1
arr = (E[]) new Object[DEFAULT_CAPACITY + 1];
capacity = DEFAULT_CAPACITY;
size = 0;
headIndex = 0;
tailIndex = 0;
}
public LoopQueue(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity Error!");
}
arr = (E[]) new Object[capacity + 1];
this.capacity = capacity;
size = 0;
headIndex = 0;
tailIndex = 0;
}
@Override
public void enqueue(E e) {
if (size == capacity) {
throw new IllegalArgumentException("Queue is Full!");
}
arr[tailIndex] = e;
size++;
tailIndex = (tailIndex + 1) % arr.length;
}
@Override
public E dequeue() {
if (size == 0) {
throw new IllegalArgumentException("Queue is Empty!");
}
E e = arr[headIndex];
arr[headIndex] = null;
size--;
headIndex = (headIndex + 1) % arr.length;
return e;
}
@Override
public E getHead() {
return arr[headIndex];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public int getSize() {
return size;
}
public int getCapacity() {
return capacity;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("head [");
for (int i = headIndex; i != tailIndex; i = (i + 1) % arr.length) {
sb.append(arr[i]);
if ((i + 1) % arr.length != tailIndex) {
sb.append(", ");
}
}
sb.append("] tail");
return sb.toString();
}
}
代码二:不使用size变量来记录队列中的元素个数。
如果不维护size变量,那么相对来说稍微有一点复杂的是在getSize这个方法上,需要找到size与headIndex、tailIndex之间的联系。
如果
,说明当前队列还没有开始“循环”,这样的话,
。
如果
,说明当前队列已经开始“循环”,这样的话,
。
public interface Queue {
/**
* 入队
*
* @param e 入队的元素
*/
void enqueue(E e);
/**
* 出队
*
* @return 返回出队的元素
*/
E dequeue();
/**
* 获取队首元素
*
* @return 队首元素
*/
E getHead();
/**
* 是否是空队列
*
* @return 返回是否是空队列
*/
boolean isEmpty();
/**
* 获取队列中元素个数
*
* @return 返回队列中元素个数
*/
int getSize();
}
/**
* 循环队列
*/
public class LoopQueue implements Queue {
/**
* 数组
*/
private E[] arr;
/**
* 队列的最大容量
*/
private int capacity;
/**
* headIndex 队首索引
* tailIndex 队尾索引
*/
private int headIndex, tailIndex;
/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
public LoopQueue() {
// 根据循环队列的约定,数组的长度比队列最大容量要多1
arr = (E[]) new Object[DEFAULT_CAPACITY + 1];
capacity = DEFAULT_CAPACITY;
headIndex = 0;
tailIndex = 0;
}
public LoopQueue(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity Error!");
}
arr = (E[]) new Object[capacity + 1];
this.capacity = capacity;
headIndex = 0;
tailIndex = 0;
}
@Override
public void enqueue(E e) {
if (headIndex == (tailIndex + 1) % arr.length) {
throw new IllegalArgumentException("Queue is Full!");
}
arr[tailIndex] = e;
tailIndex = (tailIndex + 1) % arr.length;
}
@Override
public E dequeue() {
if (tailIndex == headIndex) {
throw new IllegalArgumentException("Queue is Empty!");
}
E e = arr[headIndex];
arr[headIndex] = null;
headIndex = (headIndex + 1) % arr.length;
return e;
}
@Override
public E getHead() {
return arr[headIndex];
}
@Override
public boolean isEmpty() {
return headIndex == tailIndex;
}
@Override
public int getSize() {
return tailIndex >= headIndex ? (tailIndex - headIndex) : (tailIndex - headIndex + arr.length);
}
public int getCapacity() {
return capacity;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("head [");
for (int i = headIndex; i != tailIndex; i = (i + 1) % arr.length) {
sb.append(arr[i]);
if ((i + 1) % arr.length != tailIndex) {
sb.append(", ");
}
}
sb.append("] tail");
return sb.toString();
}
}
代码三:如果使用size变量记录队列中有多少元素,实际上完全可以不用在创建数组时比队列最大容量多出一个空间。
稍微有点复杂的是在toString方法上,也就是遍历上要注意一下:
循环队列-遍历细节
public interface Queue {
/**
* 入队
*
* @param e 入队的元素
*/
void enqueue(E e);
/**
* 出队
*
* @return 返回出队的元素
*/
E dequeue();
/**
* 获取队首元素
*
* @return 队首元素
*/
E getHead();
/**
* 是否是空队列
*
* @return 返回是否是空队列
*/
boolean isEmpty();
/**
* 获取队列中元素个数
*
* @return 返回队列中元素个数
*/
int getSize();
}
/**
* 循环队列
*/
public class LoopQueue implements Queue {
/**
* 数组
*/
private E[] arr;
/**
* 队列的最大容量
*/
private int capacity;
/**
* 队列中当前元素数目
*/
private int size;
/**
* headIndex 队首索引
* tailIndex 队尾索引
*/
private int headIndex, tailIndex;
/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
public LoopQueue() {
this(DEFAULT_CAPACITY);
}
public LoopQueue(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity Error!");
}
arr = (E[]) new Object[capacity];
this.capacity = capacity;
size = 0;
headIndex = 0;
tailIndex = 0;
}
@Override
public void enqueue(E e) {
if (size == capacity) {
throw new IllegalArgumentException("Queue is Full!");
}
arr[tailIndex] = e;
size++;
tailIndex = (tailIndex + 1) % capacity;
}
@Override
public E dequeue() {
if (size == 0) {
throw new IllegalArgumentException("Queue is Empty!");
}
E e = arr[headIndex];
arr[headIndex] = null;
size--;
headIndex = (headIndex + 1) % capacity;
return e;
}
@Override
public E getHead() {
return arr[headIndex];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public int getSize() {
return size;
}
public int getCapacity() {
return capacity;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("head [");
// 遍历逻辑有变化
for (int i = 0; i < size; i++) {
sb.append(arr[(headIndex + i) % capacity]);
if ((i + 1) != size) {
sb.append(", ");
}
}
sb.append("] tail");
return sb.toString();
}
}
如果使用size变量,“数组的长度比队列最大容量多1个空间”不是必须的;但如果不使用size变量,“数组的长度比队列最大容量多1个空间”是必要的。对于下面将论述的含“扩容”情形,这一点同样也适用。
4.2、在考虑扩容的情况下基于数组实现循环队列
当超过队列最大容量,自动将容量进行扩充;同样当队列中元素过少时,可以进行缩容。
代码:不使用size,多出一个空间
public interface Queue {
/**
* 入队
*
* @param e 入队的元素
*/
void enqueue(E e);
/**
* 出队
*
* @return 返回出队的元素
*/
E dequeue();
/**
* 获取队首元素
*
* @return 队首元素
*/
E getHead();
/**
* 是否是空队列
*
* @return 返回是否是空队列
*/
boolean isEmpty();
/**
* 获取队列中元素个数
*
* @return 返回队列中元素个数
*/
int getSize();
}
/**
* 循环队列
*/
public class LoopQueue implements Queue {
/**
* 数组
*/
private E[] arr;
/**
* 队列的最大容量
*/
private int capacity;
/**
* headIndex 队首索引
* tailIndex 队尾索引
*/
private int headIndex, tailIndex;
/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
public LoopQueue() {
// 根据循环队列的约定,数组的长度比队列最大容量要多1
arr = (E[]) new Object[DEFAULT_CAPACITY + 1];
capacity = DEFAULT_CAPACITY;
headIndex = 0;
tailIndex = 0;
}
public LoopQueue(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity Error!");
}
arr = (E[]) new Object[capacity + 1];
this.capacity = capacity;
headIndex = 0;
tailIndex = 0;
}
@Override
public void enqueue(E e) {
// 扩容
if (headIndex == (tailIndex + 1) % arr.length) {
reCapacity(capacity * 2);
}
arr[tailIndex] = e;
tailIndex = (tailIndex + 1) % arr.length;
}
@Override
public E dequeue() {
if (tailIndex == headIndex) {
throw new IllegalArgumentException("Queue is Empty!");
}
E e = arr[headIndex];
arr[headIndex] = null;
headIndex = (headIndex + 1) % arr.length;
// 缩容
if (getSize() <= capacity / 4 && capacity / 2 != 0) {
reCapacity(capacity / 2);
}
return e;
}
@Override
public E getHead() {
return arr[headIndex];
}
@Override
public boolean isEmpty() {
return headIndex == tailIndex;
}
@Override
public int getSize() {
return tailIndex >= headIndex ? (tailIndex - headIndex) : (tailIndex - headIndex + arr.length);
}
public int getCapacity() {
return capacity;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("head [");
for (int i = headIndex; i != tailIndex; i = (i + 1) % arr.length) {
sb.append(arr[i]);
if ((i + 1) % arr.length != tailIndex) {
sb.append(", ");
}
}
sb.append("] tail");
return sb.toString();
}
// 这是一种写法
private void reCapacity(int newCapacity) {
E[] newArr = (E[]) new Object[newCapacity + 1];
int j = 0;
for (int i = headIndex; i != tailIndex; i = (i + 1) % arr.length) {
newArr[j] = arr[i];
j++;
}
headIndex = 0;
tailIndex = j;
capacity = newCapacity;
arr = newArr;
}
}
reCapacity方法除了上面的写法,还可以这样写:
private void reCapacity2(int newCapacity) {
E[] newArr = (E[]) new Object[newCapacity + 1];
for (int i = 0; i < getSize(); i++) {
newArr[i] = arr[(headIndex + i) % arr.length];
}
// 由于没有 size 维护队列中元素个数,所以关键变量维护的时候要注意先后顺序
// 先维护 tailIndex 再维护 headIndex
tailIndex = getSize();
headIndex = 0;
capacity = newCapacity;
arr = newArr;
}
private void reCapacity(int newCapacity) {
E[] newArr = (E[]) new Object[newCapacity + 1];
// 如果怕变量维护出现问题,也可以先保存 size
int size = getSize();
for (int i = 0; i < getSize(); i++) {
newArr[i] = arr[(headIndex + i) % arr.length];
}
headIndex = 0;
tailIndex = size;
capacity = newCapacity;
arr = newArr;
}