1、什么是队列
队列,可以把它想象成排队买票,先来的先买,后来的后买,后来的人只能站末尾,不允许插队。先进者先出,这就是典型的「队列」。
我们知道,栈只支持两个基本操作:「入栈push」 和「出栈pop」。队列跟栈非常相似,支持的操作也很有限,最基本的操作也是两个:「入队queue」,放一个数据到队列尾部;「出队dequeue」,从队列头部取一个元素。
所以,队列跟栈一样,也是一种操作受限的线性表数据结构。
2、如何实现一个队列
队列跟栈一样,也是一种抽象的数据结构,具有先进先出的特性FIFO。它支持在队尾插入元素,在队头删除元素。队列既可以用数组来实现,也可以用链表来实现。用数组实现的队列叫作「顺序队列」,用链表实现的队列叫作「链式队列」。
2.1、顺序队列
比起栈的数据组实现,队列的数组实现稍微有点复杂。对于栈来说,我们只需要一个「栈顶指针」就可以了。但队列需要两个指针:一个是head指针,指向队头;另一个是tail指针,指向队尾。
如图所示,当[a, b, c, d]依次入队之后,队列中的 head 指针指向 0 位置(head = 0),tail 指针指向 4 位置(tail = 4)。
当我们调用两次出队操作之后,队列中的 head 指针指向 2 位置(head = 2),tail 指针指向 4 位置(tail = 4)
这个过程会有一个问题,随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组有空闲空间,也无法继续往队列中加数据了。
❝数组中使用的空间为:
❞[head, tail],当 head = 0,tail 移动到最右边时,数组无可用空间;其他情况,数组都有空闲空间
如何解决当 tail 移动到最右边,head != 0 时的问题呢?我们常用的方法就是「数据搬移」,但是,每次进行出队操作都相当于删除数组下标为 0 的数据,要搬移整个队列中的数据,这样出队操作的时间复杂度就会从原来的O(1)变为O(n)。
通常,我们会在出队时可以不用搬移数据。当 tail 已经在最右边时,我们只需在最新一次入队操作时,集中触发一次数据的搬移操作。具体实现如下:
/**
* @description: 数组实现的队列
* @author: erlang
* @since: 2020-09-07 22:12
*/
public class ArrayQueue {
// 数组:arr,数组大小:n
private int[] arr;
private int limit = 0;
// head表示队头下标,tail表示队尾下标
private int head = 0;
private int tail = 0;
// 申请一个大小为capacity的数组
public ArrayQueue(int capacity) {
arr = new int[capacity];
limit = capacity;
}
public int dequeue() {
// 如果head == tail 表示队列为空
if (head == tail) {
throw new RuntimeException("栈空了");
}
// 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了
int ret = arr[head];
++head;
return ret;
}
public boolean queue(int item) {
// tail == limit 表示队列末尾没有空间了
if (tail == limit) {
// tail ==n && head==0,表示整个队列都占满了
if (head == 0) {
throw new RuntimeException("栈满了");
}
// 数据搬移
for (int i = head; i < tail; ++i) {
arr[i - head] = arr[i];
}
// 搬移完之后重新更新head和tail
tail -= head;
head = 0;
}
arr[tail] = item;
++tail;
return true;
}
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(7);
queue.queue(1);
queue.queue(2);
queue.queue(3);
queue.queue(4);
queue.queue(5);
queue.queue(6);
queue.queue(7);
System.out.println(queue.dequeue());
queue.queue(1);
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
}
}
2.2、链式队列
基于链表的实现,我们同样需要连个指针:head和tail。它们分别指向链表的第一个结点和最后一个结点。如图所示:入队时,tail.next = new Node(); tail = tail.next;出队时,head = head.next;。
/**
* @description: 链表实现队列
* @author: erlang
* @since: 2020-08-31 20:12
*/
public class LinkedListQueue<T> {
public static class Node<T> {
public T value;
public Node<T> pre;
public Node<T> next;
public Node(T value) {
this.value = value;
}
}
public static class DoubleEndsQueue<T> {
public Node<T> head;
public Node<T> tail;
public void addFromHead(T value) {
Node<T> cur = new Node<>(value);
if (head == null) {
tail = cur;
} else {
cur.next = head;
head.pre = cur;
}
head = cur;
}
public T popFromTail() {
if (head == null) {
return null;
}
Node<T> cur = tail;
if (head == tail) {
head = null;
tail = null;
} else {
tail = tail.pre;
cur.pre = null;
tail.next = null;
}
return cur.value;
}
public boolean isEmpty() {
return head == null;
}
}
public DoubleEndsQueue<T> queue;
public int count;
public int limit;
public LinkedListQueue(int limit) {
queue = new DoubleEndsQueue<>();
this.limit = limit;
count = 0;
}
public void queue(T value) {
if (limit == count) {
throw new RuntimeException("栈满了");
}
count++;
queue.addFromHead(value);
}
public T dequeue() {
if (count == 0) {
throw new RuntimeException("栈空了");
}
count--;
return queue.popFromTail();
}
public boolean isEmpty() {
return queue.isEmpty();
}
public static void main(String[] args) {
LinkedListQueue<Integer> queue = new LinkedListQueue<>(7);
queue.queue(1);
queue.queue(2);
queue.queue(3);
queue.queue(4);
queue.queue(5);
queue.queue(6);
queue.queue(7);
//queue.push(8);
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
}
}
2.3、循环队列
前面用数组实现队列的时候,在 tail 移动到最右边时,会有数据搬移操作,这样入队操作性能会受到影响。如何避免数据搬移操作?这就是循环队列的思想。
原本数组是有头有尾的,是一条直线,现在我们把收尾相连,形成一个环这就构成了我们所说的循环队列。循环队列,顾名思义,就是一个环状的可以周而复始。
通过这种方法,我们成功避免了数据搬移操作。具体代码如下:
public static class RingArrayQueue {
private final int[] arr;
private int tail;
private int head;
private int size;
private final int limit;
public RingArrayQueue(int limit) {
arr = new int[limit];
tail = 0;
size = 0;
this.limit = limit;
}
public void queue(int value) {
if (size == limit) {
throw new RuntimeException("栈满了");
}
size++;
arr[tail] = value;
tail = nextIndex(tail);
}
public int dequeue() {
if (size == 0) {
throw new RuntimeException("栈空了");
}
size--;
int ans = arr[head];
head = nextIndex(head);
return ans;
}
public boolean isEmpty() {
return size == 0;
}
private int nextIndex(int index) {
return index < limit - 1 ? index + 1 : 0;
}
public static void main(String[] args) {
RingArrayQueue queue = new RingArrayQueue(7);
queue.queue(1);
queue.queue(2);
queue.queue(3);
queue.queue(4);
queue.queue(5);
queue.queue(6);
queue.queue(7);
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
System.out.println(queue.dequeue());
}
}
3、小结
队列最大的特点就是先进先出,主要的两个操作是入队和出队。跟栈一样,它既可以用数组来实现,也可以用链表来实现。用数组实现的叫顺序队列,用链表实现的叫链式队列。特是长得像环的循环队列。在数组实现队列的时候,会有数组搬移操作,要想解决数据搬移的问题,我们就需要像环一样的循环队列。