队列,一种作为构思算法的辅助数据结构,和栈相反,遵循FIFO即先进先出原则。
为了节省空间,提高底层数据结构的使用率(队列可以通过数组实现,也可以通过链表实现),常见的做法和其他数据结构扩容不同,并不是尾指针移至末尾就进行扩容,而是将尾指针移至0,从头开始(当然这要求底层数据结构如数组,起始的位置数据已经pop出去了,否则将进行扩容)。如下所示:
如图我们再次pop一个3的时候,并不像其他数据结构一样,立即扩容,而是将队尾指针移至0,将3这个元素push到0索引的位置:
这样做的根本目的就是为了节省空间,当然这会在算法实现上带来一系列难度。尤其在扩容的时候,假设我们在上图所示队列中,再push两个元素3,那么队尾指针到达2,对头指针不变,这个没有什么问题,但是在我们还想push一个元素的时候,这个时候如果像其他数据结构一样,进行简单的扩容是不能满足的。如下:
如果只是简单的扩容,我们再想push元素的时候,队尾指针上移,但是上移之后和队头指针冲突,我们这样的扩容是不行的。
解决思路:
1. 和其他数据结构一样,尾指针到达数组(以下都以数组实现队列为例)末尾的时候,不掉头,直接进行扩容。虽然可能大部分程序猿实现队列都是掉头,但是我们要知道这样做的根本目的是节省空间,而不是什么规范和协议必须遵守。由此引来的问题就是空间浪费问题。
2. 那么为什么大多数程序猿都选择尾指针掉头呢,因为本文所提及的扩容问题,解决方案并不难,我们可以在copy的时候,将队头指针到数组末尾的数据和数组开始到队尾指针的位置分开copy,虽然复制了两次,但是数组复制我们都调用System.arraycopy,利用他们内存地址连续,快速操作,不用循环(可自行百度数组内部寻址)。
如下所示,我们将队头至数组末尾的数据1,2,3,4,在扩容的时候,移至新数组的末尾,同时队头位置发生改变。
3. 为了节省空间,我们可以在每次push的时候,调用一个是否需要缩减容量的处理方法,比如,如果当前队列数组长度为20,但是实际数据量只有10,我们可以进行一次容量缩减,如果实在有此需要,建议将数组容量的1/2作为标准。
代码实现:
package com.dzh.learn.dataStructuresAndAlgorithm.dataStructures;
import java.util.Arrays;
/**
* 队列
* FIFO first in first out 先进先出
* 单向队列
* @author caiwm
*/
public class MyQueue {
private int[] arr;
private int defCapacity = 10;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/*** 实际长度 */
private int size;
/*** 队头 先出 */
private int queueHead;
/*** 队尾 后入 */
private int queueEnd;
public MyQueue(int initCapacity){
if (initCapacity <= 0)
throw new RuntimeException("Capacity must greater than zero.");
arr = new int[initCapacity];
}
public MyQueue(){
arr = new int[defCapacity];
}
public void push(int element){
ensureCapacityInternal();
// TODO 缩减整理 例如空闲空间大于1/2
if (size == 0){
queueHead = queueEnd = 0;
arr[0] = element;
} else{
// 最上 返回0赋值
if (queueEnd == (arr.length - 1)){
arr[0] = element;
queueEnd = 0;
} else {
arr[++queueEnd] = element;
}
}
size++;
}
private void ensureCapacityInternal(){
if (arr.length == size){
grow();
}
}
private void grow(){
// int 最大值校验
int capacityAdd = ensureExplicitCapacityAdd();
if (queueEnd < queueHead){
// 尾指针掉头 copy方案
int[] arrNew = new int[arr.length + capacityAdd];
int numMoved = arr.length - queueHead;
System.arraycopy(arr, queueHead, arrNew, (queueHead + capacityAdd), numMoved);
System.arraycopy(arr, 0, arrNew, 0, queueHead);
queueHead += capacityAdd;
arr = arrNew;
} else {
arr = Arrays.copyOf(arr, arr.length + capacityAdd);
}
}
private int ensureExplicitCapacityAdd(){
if (arr.length + 1 > MAX_ARRAY_SIZE)
return ((arr.length + 1 > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE) - arr.length;
else
return arr.length >> 1;
}
public int peek(){
if (isEmpty())
throwEmptyException();
return arr[queueHead];
}
public int pop(){
if (isEmpty())
throwEmptyException();
int i = arr[queueHead];
arr[queueHead] = 0; // 为了大家看得更直观,其实可以不需要,但是Object类型queue或者泛型queue,此处最好置为null
if (queueHead == (arr.length - 1)){
queueHead = 0;
} else {
queueHead++;
}
size--;
return i;
}
public boolean isEmpty(){
return size == 0;
}
public void throwEmptyException(){
throw new RuntimeException("Queue is empty.");
}
}