队列:以顺序的方式维护的一组数据集合,在一端添加数据,从另一端移除数据。习惯来说,添加的一端称为尾,移除的一端称为头。
通用接口
public interface Queue<E> {
/**
* 插入队列
*/
boolean offer(E value);
/**
* 从队列中获取值并移除
*/
E poll();
/**
* 从队列中获取值但不移除
*/
E peek();
/**
* 检查队列是否已满
*/
boolean isFull();
/**
* 检查队列是否不为空
*/
boolean isEmpty();
}
基于单向循环链表的简单实现
public class LinkedQueue<E> implements Queue<E>, Iterable<E> {
//提供哨兵节点
private Node<E> sentinel = new Node<E>(null, null);
//提供尾节点
private Node<E> tail = sentinel;
//队列大小
private int size = 0;
//队列容量
private int capacity = Integer.MAX_VALUE;
public LinkedQueue(int capacity) {
this.capacity = capacity;
tail.next = sentinel;
}
private static class Node<E> {
Node<E> next;
E value;
public Node(Node<E> next, E value) {
this.next = next;
this.value = value;
}
}
@Override
public boolean offer(E value) {
//在队尾插入元素,选择尾插法
if (isFull()) {
return false;
}
Node<E> node = new Node<>(sentinel, value);
tail.next = node;
tail = node;
size++;
return true;
}
@Override
public E poll() {
if (isEmpty()) {
return null;
}
Node<E> first = sentinel.next;
if (first==tail){
//如果是最后一个节点,那么将tail指向sentinel
tail =sentinel;
}
sentinel.next = first.next;
E value = first.value;
size--;
return value;
}
@Override
public E peek() {
return sentinel.next.value;
}
@Override
public boolean isFull() {
return size == capacity;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p = sentinel.next;
@Override
public boolean hasNext() {
return p != sentinel;
}
@Override
public E next() {
E value = p.value;
p = p.next;
return value;
}
};
}
}
基于循环数组的简单实现
实现前,介绍一下环形数组与数组的区别
- 对比普通数组,起点和终点更为自由,不用考虑数据移动(普通数组移除元素时需要移动其他元素)
- “环”意味着不会存在【越界】问题
- 数组性能更佳
- 环形数组比较适合实现有界队列、RingBuffer 等
public class ArraysQueue<E> implements Queue<E>, Iterable<E> {
private int head = 0;
private int tail = 0;
//用来记录循环数组大小
private final int length;
private E[] array;
@SuppressWarnings("all")
public ArraysQueue(int capacity) {
this.length = capacity + 1;
//加一是为尾指针留一个空间去判断是否队列已满
this.array = (E[]) new Object[length];
}
@Override
public boolean offer(E value) {
if (isFull()) {
return false;
}
array[tail++] = value;
tail = tail % length;
return true;
}
@Override
public E poll() {
if (isEmpty()) {
return null;
}
E value = array[head];
head = (head + 1) % length;
return value;
}
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[head];
}
@Override
public boolean isFull() {
if ((tail + 1) % length == head) {
return true;
}
return false;
}
@Override
public boolean isEmpty() {
return head == tail;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
@Override
public boolean hasNext() {
return p != tail;
}
@Override
public E next() {
E value = array[p];
p = (p + 1) % length;
return value;
}
};
}
}
在Java源码中的基于数组实现的队列对容量有一个要求,即一定是2的n次方。之所以这么要求,是因为方便头指针和尾指针的边界确认。
我们的实现方式中指针的值是通过+1并取余来确定指针的下一个位置,也就是说,head和tail的值始终是在数组长度中。而在Java源码中,并没有规定head与tail的取值一定是数组长度内,而是不停的+1然后通过对数组长度的取余,来确定head与tail的下标位置。
但是这样又存在一个问题。那就是head或是tail超过了int类型所能表达的最大值后,再去取余会得到负数,使用负数去数组中拿元素会报错。为了解决这个问题,Java针对二进制特点采用了更高效率的实现方案。
首先就是规定数组长度一定是2的n次方。
看下面例子
因此我们不需要在意符号位是否为负数,只需要关心余数的二进制即可。
对于如何通过二进制的方式获取到余数。是二进制的另一个特性
我们查看ArrayDeque源码中的添加元素方法
正是采用了二进制的位运算特性来控制head与tail在数组中的下标位置。如果用户指定数组队列不是一个2的n次方时,他会强制扩容到最近的2的n次方大小。具体实现方式如下