“后进先出”,“先进先出“ 我们经常会听见这两句。然而这两句话代表了两种不同的数据结构,前者我们已经熟悉,接下来我们就探究一下后者。
要点
一、队列(以数组队列为例)
概念:
队列是一种先进先出(FIFO)的数据结构。
特点:
队列内的数据,先入队的取元素时先出去(参考下生活实例图理解)。
与栈的区别(模型)
表面区别
1 相同之处 :规定好首端尾端后,都是把数据添加到尾端(没有数据时默认首也是尾端理解)
2 不同之处: 一个从首端取数据,一个从尾端取数据,取数据位置不同。
生活实例图:
队列的设计
通常用户(面向客户端程序员)是不关心我们具体实现的细节的,他们关心的时具有队列的功能,能够使用队列,所以本队列就以动态数组为基础。
特有api
构建队列接口
package queue;
/**
* Create by SunnyDay on 2019/02/06
* <p>
* 队列接口(注意java 原生的Queue接口继承了Collection接口)
*/
public interface Queue<E> {
E dequeue();
E getFront();
int getSize();
void enqueue(E e);
boolean isEmpty();
}
数组队列的实现(自定义)
我们遵循队列的概念先进先出很容易就设计出两个主要的api----入队、出队。
1 入队元素添加队尾
2 出队移除首个元素,也就是第一个元素
于是如下就可以很简单的设计出基础的数组队列。
package queue;
import array.UseGeneric;
/**
* Create by SunnyDay on 2019/02/06
*/
public class ArrayQueue<E> implements Queue<E> {
private UseGeneric<E> arrayQueue ;
public ArrayQueue(int capacity){
arrayQueue = new UseGeneric<>(capacity);
}
public ArrayQueue(){
arrayQueue = new UseGeneric<>();
}
/**
* 出队
*/
@Override
public E dequeue() {
return arrayQueue.removeFirst();
}
/**
* 入队
*/
@Override
public void enqueue(E e) {
arrayQueue.addLast(e);
}
/**
* 查看队的首个元素
* */
@Override
public E getFront() {
return arrayQueue.getFirst();
}
/**
* 当前队的大小
* */
@Override
public int getSize() {
return arrayQueue.getSize();
}
/**
* 是否为空
* */
@Override
public boolean isEmpty() {
return arrayQueue.isEmpty();
}
/**
* 队列容量
* */
public int getCapacity(){
return arrayQueue.getCapacity();
}
/**
*
* 队列 toString 特有
* */
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Queue: ");
sb.append("front[");
for (int i = 0; i < arrayQueue.getSize(); i++) {
sb.append(arrayQueue.get(i));
if (i!=arrayQueue.getSize()-1){
sb.append(", ");
}
}
sb.append("] :tail");
return sb.toString();
}
}
分析:
一顿操作猛如虎,三下五除二就把简单的数组队列设计出来了,然而我们带分析分析我们设计的队列。
1 首先看看入队还好说,每次只添加队尾,对于入队操作的时间复杂度不大。
2 再次看看出队操作,每次移除队首元素,那么问题来了,移除一下,后面的所有元素都要向前移动,如果数量小还可以,当数量很大时,移除多次,这个操作量还是巨大的。这时我们对于数组队列要采取优化措施,于是我们引入循环队列。
二 、循环队列
通过上面的分析我们就大概了解数组队列的弊端,于是我们就构建循环队列。首先看设计思路图解:
通过图解我们大概了解了循环队列的实现思路然而我们还需注意:
1 空队列时我们规定 front=tail都指向索引为0的元素位置
2 然而当front=tail时还可能队列为满所以我们在容量上设计,使tail=front+1时队列为满(还有弊端,到下面解决,先留个坑。)
于是我们不在使用我们以前封装好的数组由于此处添加了front,tail成员我们重新建立个类LoopQueue(循环队列)
package queue;
/**
* Create by SunnyDay on 2019/02/06
* 循环队列(降低出队的时间复杂度)
*/
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front;
private int tail;
private int size;
public LoopQueue(int capacity) {
// 多出来的一个容量(不使用) 便于维护循环队列
data = (E[]) new Object[capacity + 1];
// 初始化 (不做系统会帮你做)
front = 0;
tail = 0;
size = 0;
}
public LoopQueue() {
// 多出来的一个容量(不使用) 便于维护循环队列
this(10);
}
}
以上我们设计了循环队列类,还是使用动态数组为底层封装,front为首元素指针,tail为末尾元素后一位置指针。(队列空默认front=tail=0)
重要方法入队enqueue(E e)
思路:判断队是否列满,满了扩容。反之元素入队
分析:上述我们考虑利用前面出队的空间得出tail = front+1为队列满,但是我们发现:
最终结论:
- front = (tail+1)%data.length 来判断是否队列满
- 当前front = front数值%data.length
- 当前tail = tail数值%data.length
ps:学过安卓的可以以imageview显示四张图片为例,设定一个按钮,每次点击按钮切换一张图片,四张按顺序循环切换。自己动手推导一遍。(没搞过得自己搞个简单数组循环重复输出数组内的数字一样)
于是:
/**
* 循环队列重要方法:入队
*/
@Override
public void enqueue(E e) {
// 判断队列是否满了,满了就扩容
if (front == (tail + 1) % data.length) {
// 扩大为原来的2倍
resize(getCapacity() * 2);
}
data[tail] = e; // 队列中添加元素
tail = (tail + 1) % data.length;// 重置tail的大小(%是为了防止越界)
size++;
}
/**
* 重置 容量(扩容)
*/
private void resize(int newCapacity) {
// 一样的道理,多申请个空间
E[] newData = (E[]) new Object[newCapacity + 1];
// 循环旧的数组进行
for (int i = 0; i < size; i++) {
// 原来的front并不一定在索引为0的位置,而是存在front+i的关系
newData[i] = data[(front + i) % data.length];//防止越界(大小0 到 size-1 )
}
data = newData;
front = 0;
tail = size;
}
可以看出满时我们先扩容再入队:
扩容注意循环遍历元素的坑( newData[i] = data[(front + i) % data.length])
扩容后进行入队
有了入队出队就好理解了
/**
* 循环队列重要方法: 出队
*/
@Override
public E dequeue() {
if (isEmpty()) {
throw new IllegalArgumentException("can not dequeue ,loopQueue is empty");
}
E temp = data[front];
data[front] = null;// 从队列移除
front = (front + 1) % data.length;// 重置front大小
size--;
// 缩容处理(缩小大小不能为0)
if (size == getCapacity() / 4 && getCapacity() / 2 != 0) {
resize(getCapacity() / 2);
}
return temp;
}
LoopQueue完整方法如下:
package queue;
/**
* Create by SunnyDay on 2019/02/06
* 循环队列(降低出队的时间复杂度)
*/
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front;
private int tail;
private int size;
public LoopQueue(int capacity) {
// 多出来的一个容量(不使用) 便于维护循环队列
data = (E[]) new Object[capacity + 1];
// 初始化 (不做系统会帮你做)
front = 0;
tail = 0;
size = 0;
}
public LoopQueue() {
// 多出来的一个容量(不使用) 便于维护循环队列
this(10);
}
/**
* 容量
*/
public int getCapacity() {
//循环队列中有个元素位置不使用
return data.length - 1;
}
/**
* 循环队列是否为空
*/
public boolean isEmpty() {
return front == tail;
}
/**
* 队列大小
*/
@Override
public int getSize() {
return size;
}
/**
* 循环队列重要方法: 出队
*/
@Override
public E dequeue() {
if (isEmpty()) {
throw new IllegalArgumentException("can not dequeue ,loopQueue is empty");
}
E temp = data[front];
data[front] = null;// 从队列移除
front = (front + 1) % data.length;// 重置front大小
size--;
// 缩容处理(缩小大小不能为0)
if (size == getCapacity() / 4 && getCapacity() / 2 != 0) {
resize(getCapacity() / 2);
}
return temp;
}
/**
* 循环队列重要方法:入队
*/
@Override
public void enqueue(E e) {
// 判断队列是否满了,满了就扩容
if (front == (tail + 1) % data.length) {
// 扩大为原来的2倍
resize(getCapacity() * 2);
}
data[tail] = e; // 队列中添加元素
tail = (tail + 1) % data.length;// 重置tail的大小(%是为了防止越界)
size++;
}
@Override
public E getFront() {
if (isEmpty()) {
throw new IllegalArgumentException("loopQueue is empty!");
}
return data[front];
}
/**
* 重置 容量(扩容)
*/
private void resize(int newCapacity) {
// 一样的道理,多申请个空间
E[] newData = (E[]) new Object[newCapacity + 1];
// 循环旧的数组进行
for (int i = 0; i < size; i++) {
// 原来的front并不一定在索引为0的位置,而是存在front+i的关系
newData[i] = data[(front + i) % data.length];//防止越界(大小0 到 size-1 )
}
data = newData;
front = 0;
tail = size;
}
@Override
public String toString() {
// System.out.println(Arrays.toString(data)); 默认会把没有的元素默认为0
// 自定义 封装
StringBuilder sb = new StringBuilder();
sb.append(String.format("Queue size = %d ,capacity = %d\n", size, getCapacity()));
sb.append("front[");
// 数字中的有效元素遍历
for (int i = front; i != tail; i = (i + 1) % data.length) {
sb.append(data[i]);
// 不是最后一个元素时拼接
if ((i + 1) % data.length != tail) {
sb.append(",");
}
}
sb.append("]tail");
return sb.toString();
}
// TODO toString 与 resize 使用了两种不同的遍历方式
}
优点(出队比较):
我自己列举了一些数据测试,普通队列耗时大约8秒,循环队列大约耗时0.06秒差距一目了然。
小结:
本文队列的设计主要难点为细节的掌握,循环队列的一些细节推导我们掌握了,就很容易理解了,其次理解后我们多动手搞几遍就熟悉了。
The end
细节决定成败 态度决定高度