队列
队列是一顺序的线性存储结构(线性表),具有如下特点
- 队列中的数据元素遵循先进先出(Fist In First Out)的原则,即FIFO结构
- 队列在队尾添加元素,在对头删除元素
队列相关概念
-
队头与队尾
队列允许元素插入的一端称为队尾(tail),允许元素删除的一端称为队头(front)。
如图队列的概念就像我们去银行办理业务排队一样,队伍中有队头和队尾,先来的先办业务,后来的需要从队尾进入队伍中(入队)排队等待让先来的办理完业务,队头的办理完业务从队头离开(出队)。
-
入队
队列元素的插入操作。如图元素4进入队列
-
出队
队列元素的删除操作,如图元素1出队列
队列的实现
队列是一个线性的数据机构,并且这种数据结构只允许在一端进行插入,另一端进行删除,一般情况下禁止直接访问出这两端之外的一切数据,遵循先进先出的数据结构原则。
队列实现存储数据的实现方式有一下两种:
基于顺序表(数组)的基础来实现队列,可以更加直观的去观察队列。
1. 首先定义出数组,代码如下:
public class Array<E> {
private int size;
private E[] data;
/**
* 初始化构造函数
*/
public Array() {
this(10);
}
/**
* 初始化数组容量大小构造函数
*
* @param capacity
*/
public Array(int capacity) {
data = (E[]) new Object[capacity];
size = 0;
}
/**
* 获取数组的大小
*
* @return
*/
public int getSize() {
return this.size;
}
/**
* 获取数据的容量
*
* @return
*/
public int getCapacity() {
return data.length;
}
/**
* 判断输出是否为空
*
* @return
*/
public boolean isEmpty() {
return getSize() == 0;
}
/**
* 在数组末尾添加元素
*
* @param element
*/
public void addLast(E element) {
add(size, element);
}
/**
* 在数组的开始位置添加一个元素
*
* @param element
*/
public void addFirst(E element) {
add(0, element);
}
/**
* 在任意index位置添加元素
*
* @param index
* @param element 4,5,3,2,1,6
*/
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new RuntimeException("Add element is failed,Required index >=0 and index =< size");
}
if (index == getCapacity()) {
//expandCapacity(getCapacity() * 2);
reSize();
/*throw new RuntimeException("Add failed,this Array is full");*/
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = element;
size++;
}
/**
* 扩容
* @param newSize
*/
private void expandCapacity(int newSize){
E newArray[] = (E[]) new Object[newSize];
//循环原有的数组放到新数组中
for (int i = 0; i < data.length; i++) {
newArray[i] = data[i];
}
data = newArray;
}
/**
* 扩容
* 使用java内部api扩容
*/
private void reSize(){
E newArray[] = (E[]) new Object[data.length * 2];
System.arraycopy(data,0,newArray,0,data.length);
data = newArray;
}
/**
* 返回给定元素的索引值
*
* @param element
* @return
*/
public int search(E element) {
for (int i = 0; i < size; i++) {
if (data[i] == element) {
return i;
}
}
return -1;
}
/**
* 移除数组中的第一个元素
*
* @return 返回被移除的元素
*/
public E removeFirst() {
return remove(0);
}
/**
* 移除数组的最后一个元素
*
* @return
*/
public E removeLast() {
return remove(size - 1);
}
/**
* 移除数组中的index位置上元素
*
* @param index 索引值
* @return 返回被移除的元素
*/
public E remove(int index) {
// 校验index的合法性
if (isEmpty()) {
throw new IllegalStateException("Remove() is failed,Array is empty!");
}
if (index < 0 || index > size) {
throw new IllegalStateException("Remove() is failed,the index must to great than zero and less than size");
}
E oldValue = data[index];
for (int i = index; i < size; i++) {
data[i] = data[i + 1];
}
size--;
return oldValue;
}
/**
* 根据索引获取数组中元素
*
* @param index
* @return
*/
public E get(int index) {
// 校验index是否合法
if (index < 0 || index > size) {
throw new RuntimeException("Get element is Failed,Required index >=0 and index < data.length");
}
return data[index];
}
/**
* 获取数组中第一个元素
*
* @return
*/
public E getFirst() {
return get(0);
}
/**
* 获取数据中最后一个元素
*
* @return
*/
public E getLast() {
return get(size - 1);
}
/**
* 更新索引位 index 的值
*
* @param index
* @param element
*/
public void set(int index, E element) {
// 校验index是否合法
if (index < 0 || index > getCapacity()) {
throw new RuntimeException("Get element is Failed,Required index >=0 and index < data.length");
}
data[index] = element;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
String arrInfo = String.format("Array capacity = %d size = %d", getCapacity(), getSize());
System.out.println(arrInfo);
sb.append("[");
for (int i = 0; i < this.size; i++) {
sb.append(data[i]);
if (i != size - 1) {
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
2. 有了Array<E>数组类,接下来定义一个队列的接口抽象出队列常用的方法。
public class ArrayQueue<E> implements Queue<E> {
// 基于数据实现队列
private Array<E> data;
public ArrayQueue() {
this(10);
}
public ArrayQueue(int capacity) {
data = new Array<>(capacity);
}
@Override
public void enqueue(E element) {
data.addLast(element);
}
@Override
public E dequeue() {
return data.removeFirst();
}
@Override
public E getFront() {
return data.getFirst();
}
@Override
public int getSize() {
return data.getSize();
}
public int getCapacity() {
return data.getCapacity();
}
@Override
public boolean isEmpty() {
return data.isEmpty();
}
}
3. 码出队列的具体实现
这里使用的数组作为底层数据结构去实现队列因此使用 ArrayQueue<E> 作为类命名,并且实现队列接口Queue<E>。直接上代码
public class ArrayQueue<E> implements Queue<E> {
// 基于数据实现队列
private Array<E> data;
public ArrayQueue() {
this(10);
}
public ArrayQueue(int capacity) {
data = new Array<>(capacity);
}
@Override
public void enqueue(E element) {
data.addLast(element);
}
@Override
public E dequeue() {
return data.removeFirst();
}
@Override
public E getFront() {
return data.getFirst();
}
@Override
public int getSize() {
return data.getSize();
}
public int getCapacity() {
return data.getCapacity();
}
@Override
public boolean isEmpty() {
return data.isEmpty();
}
}
以上就是使用java 来实现队列了,代码实现非常简单,因为具体的入队/出队逻辑实现都在Array<E>中。我们只需要在ArrayQueue<E>中引入Array<E> 作为成员变量 data,调用数组封装好的 addList()、removeFirst()方法就可以了。以下是队列的具体入队和出队方法的解析。
入队
@Override
public void enqueue(E element) {
data.addLast(element);
}
队列的enqueue()方法中调用了Array<E> addLast() 方法,在数组的最后面追加一个元素, 如图
根据上图来确定下实现逻辑,需要在数组中任意位置(index)插入元素(element)。以下是数组新增方法add()的主要逻辑
-
当index < 0 或者 > size(元素中数组的大小) 跑出异常。
-
当index 等于数组的初始化大小时(index = capacity),对原有数组进行扩容。
-
先看下数组添加元素的逻辑:
- 循环索引数组的每一个元素。
- 如果index 不等于 size,则需要将大于等于index的元素往后移动。
- 循环数组将index 位置往后的元素向后移动,最后
size++。
经过一通分析数组中add()实现逻辑代码如下
/**
* 在任意index位置添加元素
*
* @param index
* @param element 4,5,3,2,1,6
*/
public void add(int index, E element) {
if (index < 0 || index > size) {
throw new RuntimeException("Add element is failed,Required index >=0 and index =< size");
}
if (index == getCapacity()) {
//expandCapacity(getCapacity() * 2);
reSize();
/*throw new RuntimeException("Add failed,this Array is full");*/
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = element;
size++;
}
队列的入队是在队尾插入元素,因为我们是通过数组作为底层结构实现入队,所以只需要在队尾插入元素(index = size)。
出队
@Override
public E dequeue() {
return data.removeFirst();
}
在dequeue()方法中调用了Array<E> removeFirst() 方法,在数组的最前面(index = 0)移除一个元素, 如图
根据画图知道需要在数组的任意位置删除一个元素,主要有一下几个步骤
-
先判断数组是否为空。
-
判断传入的索引值是否合规
-
先取出需要删除的值,循环数组将index位置往后的所有元素向前移动,并
size--,然后返回先前取到的值。而队列的出队操作是在队首,只需要固定index的值(index = 0)即可。
数组中remove()实现逻辑代码如下
/**
* 移除数组中的index位置上元素
*
* @param index 索引值
* @return 返回被移除的元素
*/
public E remove(int index) {
// 校验index的合法性
if (isEmpty()) {
throw new IllegalStateException("Remove() is failed,Array is empty!");
}
if (index < 0 || index > size) {
throw new IllegalStateException("Remove() is failed,the index must to great than zero and less than size");
}
E oldValue = data[index];
for (int i = index; i < size; i++) {
data[i] = data[i + 1];
}
size--;
return oldValue;
}
时间复杂度分析
根据接口Queue 发现常用的方法有5个,那么这五个操作的时间复杂度如何呢???
public void enqueue(E element);
上面对enqueue()入队方法的解析,调用的是Array的add(int index,E e)。对add()方法解析可以得出 index 的取值范围是[0,size(n)]。在平时使用时,index的取值是有概率的,要么越接近于0要么越接近size一半概率。复杂度可以定义为O(n/2) 这时我们可以认为add()方法的复杂为O(n)。当然队列的的入队操作是写死的index=size,那么可以认为 enqueue()复杂度为: O(1)。
public E dequeue();
根据先前对dequeue()方法的逻辑了解,可以分析出每次取出数组的第一个元素时,数组中从第二个元素开始都要向前移。那么可以认为dequeue()的复杂度为:O(n)。
public E getFront();
复杂度为:O(1)
public int getSize();
复杂度为:O(1)
public boolean isEmpty();
复杂度为:O(1)
由时间复杂度的分析衍生出循环队列
上面内容用数组作为底层结构实现队列,通过对复杂度的分析发现出队(dequeue())方法的复杂度O(n),那么是否可以对这个复杂度(性能)进行优化呢?例如时间复杂由O(n)变成O(√2)、O(logn)、O(1)。显然是有的,接下来来了解下循环队列。
循环队列是什么
循环队列顾名思义将一个线性的数组想象成一个环,头尾衔接。如图:
注:图片来自网络
基于数组循环队列的实现
1、首先我们先定义一个循环队列实现Queue<E>接口,代码如下
public class CircleQueue<E> implements Queue<E> {
}
2、分析需要哪些变量
上面的代码很简单接下来我们在里面填充变量和方法。分析需要哪些变量来维护这个循环队列。根据先前我们的对队列概念的了解,在队列中主要有两个变量front,tail分别来维护队列的出队和入队操作,因为是通过数组来作为队列的底层结构还需要一个 Array<E> data,队列的长度 size,根据这些变量来丰富下CircleQueue类。
public class CircleQueue<E> implements Queue<E> {
private E[] data;
private int size;
private int front;
private int tail;
public CircleQueue() {
}
public CircleQueue(int capacity) {
data = (E[]) new Object[capacity + 1];
size = 0;
front = 0;
tail = 0;
}
}
3、在循环队列中添加元素/移除解析
根据以上代码当我们new CirCleQueue()时,是一个空的循环队列。细心的小伙伴可能发现在类的有参构造中初始化数据时加了一个单位容量这是为何呢?这里先买个小关子稍后解答。我们先看初始后循环队列的样子,如图:
循环队列初始化时,front、tail是相等的。
往循环队列中添加元素时,front、tail 的变化,如图:
添加元素时有可能让队列元素中的个数达数组的容量
capacity,如果还是使用front =tail来判断队列是否满了,显然是我们不愿意看到,所以在初始化数组的时候需要让capacity + 1。通过 tail + 1 = front 来判断循环队列是否满了。 但是tail + 1 = front并不能满足循环队列,我们需要对取余来判断因为它是循环的tail可以回到开始的位置(tail + 1)%data.length = front。
移除循环队列中元素时,font,tail 的变化,如图:
观察以上循环队列的变化发现和实现数组实现普通的队列不一样的地方在于当在队首删除某个元素不需要循环数组去移动元素,而是直接改变front的值来让循环队列发生变化是可以循环使用的一个过程。
4、代码实现
package com.chou.datastructure.queue;
/**
* @ClassName CircleQueue
* @Description 数组环形队列实现
* @Author Axel
* @Date 2021/5/2 17:01
* @Version 1.0
*/
public class CircleQueue<E> implements Queue<E> {
private E[] data;
private int size;
private int front;
private int tail;
public CircleQueue() {
}
public CircleQueue(int capacity) {
data = (E[]) new Object[capacity + 1];
size = 0;
front = 0;
tail = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return front == tail;
}
/**
* 判断队列是否满了
*
* @return
*/
public boolean isFull() {
return (tail + 1) % data.length == front;
}
/**
* 返回的是数组的长度 arr.length
*
* @return
*/
private int getCapacity() {
return data.length - 1;
}
/**
* 入队
*
* @param element
*/
@Override
public void enqueue(E element) {
if (isFull()) {
resize(getCapacity() * 2);
}
data[tail] = element;
tail = (tail + 1) % data.length;
size++;
}
/**
* 出队
*
* @return
*/
@Override
public E dequeue() {
if (isEmpty()) {
throw new IllegalStateException("Dequeue() failed,this CircleQueue is empty queue");
}
E oldValue = data[front];
data[front] = null;
front = (front + 1) % data.length;
size--;
if (size == getCapacity() / 4 && getCapacity() / 2 != 0) {
resize(getCapacity() / 2);
}
return oldValue;
}
/**
* 获取队首元素
*
* @return
*/
@Override
public E getFront() {
if (isEmpty()) {
throw new IllegalStateException("Dequeue() failed,this CircleQueue is empty queue");
}
return data[front];
}
/**
* 扩容接口
*
* @param newCapacity
*/
private void resize(int newCapacity) {
E[] newArr = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
data[i] = data[(i + front) % data.length];
}
data = newArr;
front = 0;
tail = size;
}
@Override
public String toString() {
StringBuffer sBuff = new StringBuffer();
sBuff.append(String.format("Circle Queue = %d capacity= %d\n", size, getCapacity()));
sBuff.append("front [");
for (int i = front; i != tail; i = (i + 1) / data.length) {
sBuff.append(data[i]);
if ((1 + i) % data.length != tail) {
sBuff.append(",");
}
}
sBuff.append("] tail");
return sBuff.toString();
}
}
循环队列复杂度分析
public void enqueue(E element);
复杂度为:O(1)
public E dequeue();
复杂度为: O(1)
public E getFront();
复杂度为:O(1)
public int getSize();
复杂度为:O(1)
public boolean isEmpty();
复杂度为:O(1)
队列还可以通过链表、栈来实现这里就不一一列举了,有兴趣的小伙伴可以自行去学习。本篇文章到这里就结束了,文章中理解有出入的地方还行各位大佬指正,谢谢!