数据结构学习笔记 - 04 队列
介绍
队列的的操作也是数组的子集,不同的是,队列只能从一端(队尾)添加元素,只能从另一端(队首)取出元素。
队列实现
将队列定义为一个接口:
public interface Queue<E> {
/**
* 入队。
*
* @param e 入队的元素。
*/
void enqueue(E e);
/**
* 出队。
*
* @return 出队的元素。
*/
E dequeue();
/**
* 获得队首元素。
*
* @return 队首的元素。
*/
E getFront();
/**
* 获得队列中元素的个数。
*
* @return 元素的个数。
*/
int getSize();
/**
* 查看队列是否为空。
*
* @return 队列是否为空。
*/
boolean isEmpty();
}
使用动态数组实现队列:
/**
* ArrayQueue
*
* 类似栈的实现,这里也使用动态数组对队列进行一个实现。
*
* 这个队列中的五个方法复杂度:
* void enqueue(E) O(1) 均摊
* E dequeue() O(n)
* E getFront() O(1)
* int getSize() O(1)
* boolean isEmpty() O(1)
*
* 可以看到,数组实现的队列最大问题就是出队的复杂度是 O(n)
*/
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();
}
/**
* 查看实现这个队列的这个动态数组当前的容积。
*/
public int getCapacity() {
return array.getCapacity();
}
/**
* 入队。
*/
@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 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();
}
}
使用循环队列解决数组队列出队复杂度高的问题:
/**
* LoopQueue
* 数组队列的问题在于,出队的时间复杂度过高,导致如果队列元素特别多的情况下,出队就比较耗时。
* 循环队列可以解决这一问题。
*
* 这个队列中的五个方法复杂度:
* void enqueue(E) O(1) 均摊
* E dequeue() O(1) 均摊
* E getFront() O(1)
* int getSize() O(1)
* boolean isEmpty() O(1)
*/
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front, tail;
private int size;
@SuppressWarnings("unchecked")
public LoopQueue(int capacity) {
// 创建的数组容量比用户期望的多一个,因为循环队列中,我们会有意识的浪费掉一个空间。
// 因为我们通常认为 front == tail 的时候是队列为空,对于循环队列,我们如果不让出一个空间,那么这个表达式也有可能是队列元素占满的情况。
data = (E[]) new Object[capacity + 1];
front = 0;
tail = 0;
size = 0;
}
public LoopQueue() {
this(10);
}
/**
* 循环队列创建的数组中,我们会有意识地浪费掉一个空间。
*
* @return
*/
public int getCapacity() {
return data.length - 1;
}
/**
* 循环队列使用 front == tail 来判断元素为空,这也是为什么创建数组需要多一个空间。
*/
@Override
public boolean isEmpty() {
return front == tail;
}
/**
* 获得队列元素的个数。
*/
@Override
public int getSize() {
return size;
}
/**
* 入队。
*/
@Override
public void enqueue(E e) {
// 判断队列是否满了
if ((tail + 1) % data.length == front) {
resize(getCapacity() * 2);
}
data[tail] = e;
tail = (tail + 1) % data.length;
size++;
}
/**
* 出队。
*/
@Override
public E dequeue() {
if (isEmpty()) {
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
}
E ret = data[front];
data[front] = null;
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];
}
@SuppressWarnings("unchecked")
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity + 1];
// 第一种遍历循环队列的所有元素的方式。
for (int i = 0; i < size; i++) {
// 当扩容或者缩容的时候,原本 front 不为 0 的队列被重新规整为 front 为 0 的队列了
newData[i] = data[(i + front) % data.length];
}
data = newData;
front = 0;
tail = size;
}
/**
* 打印该类的显示字符串。
*/
@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();
}
}
使用链表实现队列:
/**
* LinkedListQueue
*
* 这里我们使用链表实现队列。
* 由于队列需要从一端添加,另一端删除,对于链表来说,我们需要给它先添加一个 tail 标记,类似 head 标记那样,这样我们从末尾添加元素就也是 O(1)
* 级别的了。
* 但是有一个问题是,即使添加了 tail 标记,在从尾部删除一个元素的时候,我们无法快速找到 tail 之前的那个节点。
*
* 所以合理的设计队列的方式就是,从尾部只负责添加元素,在链表的头部负责删除元素,也就是出队。
*
* 这里我们设计的链表就不使用 dummyHead 了,但是要注意链表为空的情况。
*/
public class LinkedListQueue<E> implements Queue<E> {
private class Node {
public E e;
public Node next;
public Node(E e, Node next) {
this.e = e;
this.next = next;
}
public Node(E e) {
this(e, null);
}
// public Node() {
// this(null);
// }
@Override
public String toString() {
return e.toString();
}
}
private Node head, tail;
private int size;
public LinkedListQueue() {
head = null;
tail = null;
size = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void enqueue(E e) {
if (tail == null) {
tail = new Node(e);
head = tail;
} else {
tail.next = new Node(e);
tail = tail.next;
}
size++;
}
@Override
public E dequeue() {
if (isEmpty()) {
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
}
Node retNode = head;
head = head.next;
retNode.next = null;
if (head == null) {
tail = null;
}
size--;
return retNode.e;
}
@Override
public E getFront() {
if (isEmpty()) {
throw new IllegalArgumentException("Queue is empty.");
}
return head.e;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Queue: front ");
Node cur = head;
while (cur != null) {
res.append(cur + "->");
cur = cur.next;
}
res.append("NULL tail");
return res.toString();
}
}