循环队列
先不急着给循环队列的定义,先看队列循环存储结构的弊端。
队列顺序存储结构的弊端
队列的顺序存储结构本身是由ArrayList实现的,在数据元素入队的时候,相当于在ArrayList表尾添加元素,在数据元素出队的时候,相当于在ArrayList表头删除元素。很明显,入队的时间复杂度O(1),出队的时间复杂度O(n),线性表增删数据元素时间复杂符都是O(n),但是这个是按平均算的。队列的出队时间复杂度O(n),可不是按平均算的,因为每次出队都是O(n)。
有弊端,就可以换其他思路进行优化
优化一:为了避免当只有一个元素时,队头队尾重合处理变得麻烦,所有引入两个指针,front指向队头元素,rear指向队尾元,让队头指针和队尾指针随着元素的变化而移动。这样我们入队和出队操作都是O(1)。
问题:rear指针到了表尾就不能后移了,而且出队front一直后移,前面的空间就浪费了。
优化二:当队头或队尾指针到达尾部时,如需后移可重新指向表头,就相当于是把线性表首尾相连,变成一个环,指针从尾到头有一个周期性,也就是数线性表长度,指针+1对线性表长度取余就可以完成指针从头到尾的跳跃,这样就解决了空间浪费的问题。
问题:但是又有新的问题,我们判断队列满和队列空的条件都是(rear+1)%n==front,这里n是线性表的长度,这就没法做正确的判断了。
优化三:将一个空间预留出来不存任何元素,尾指针始终指向这个null空间,这时,队列满的条件(rear+1)%n==front,队列为空的条件是rear == front,正解。
其实以上的优化结果就是循环队列,我们把队列的这种头尾相接的顺序存储结构称为循环队列。
用Java代码实现循环队列的顺序存储结构,新建的ArrayQueueLoop实现Queue接口,重写其中的方法,代码如下,注释详细
import java.util.Iterator;
//循环队列 底层用动态数组实现
public class ArrayQueueLoop<E> implements Queue<E> {
private E[] data; //定义数组
private int front; //头指针
private int rear; //尾指针 指向
private int size; //有效元素个数
public ArrayQueueLoop(){
data= (E[]) (new Object[11]);//因为有一个空的空间,有效能存储的空间有10个
front=0; //初始化
rear=0;
size=0;
}
@Override
public int getSize() {
return size; //获取有效元素个数,直接返回size即可
}
@Override
public boolean isEmpty() {
return size==0&&front==rear; //循环队列为空的条件
}
@Override
public void enqueue(E e) {
if((rear+1)%data.length==front){ //队列满时
resize(2*data.length-1); //扩容,扩二倍
}
data[rear]=e; //把e从队尾入队列,rear角标处是空的空间
rear=(rear+1)%data.length; //更新尾指针,rear重新指向空
size++; //有效元素+1
}
@Override
public E dequeue() {
if(isEmpty()){ //队列空时不可出队,抛异常
throw new IllegalArgumentException("队列空");
}
E ret=data[front]; //变量ret存头指针的元素,用于返回
front=(front+1)%data.length; //更新头指针
size--; //有效元素-1
//当有效元素小于等于能存储的数组长度,同时缩容后长度的大于等于10,方可进行缩容操作
if(size<=(data.length-1)/4&&(data.length-1)/2>=10){
resize((data.length-1)/2+1); //缩容缩一半
}
return ret; //返回被删除的元素
}
//扩缩容函数
private void resize(int newLen){
E[] newData= (E[]) (new Object[newLen]); //创新数组,即缩容后的数组
int p=front; //用指针p遍历原数组
int i=0; //用指针i遍历新数组
while(true){
newData[i]=data[p]; //把原数组中的元素复制给新数组
i++;
p=(p+1)%data.length;
if(p==rear){ //p指针遍历的尾指针处结束循环
break;
}
}
front=0; //把头指针指向扩容后数组表头位置
rear=size; //尾指针指向有效元素长度角标
data=newData; //把新数组给元素组,偷梁换柱
}
@Override
public E getFront() {
if(isEmpty()){
throw new IllegalArgumentException("队列为空");
}
return data[front]; //获取队头,队头就是头指针处
}
@Override
public E getRear() {
if(isEmpty()){
throw new IllegalArgumentException("队列为空");
}
return data[(rear-1+data.length)%data.length]; //获取队尾,队尾是尾指针的前一个,尾指针始终指向空
}
//清空队列元素,把构造函数里的代码拿过来(初始化状态)
@Override
public void clear() {
data= (E[]) (new Object[11]);//因为有一个空的空间
front=0;
rear=0;
size=0;
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder(); //创建StringBuilder对象,可以直接在原字符串上添加字符,不许要每次改变都创新字符串
/*
输出这样的格式
ArrayQueueLoop: 15/20
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
*/
sb.append(String.format("ArrayQueueLoop: %d/%d\n",size,data.length-1));
sb.append('[');
if(isEmpty()){
sb.append(']');
}else{
for(int i=front;i!=rear;i=(i+1)%data.length){ //打印元素,从头指针处开始,到达指针结束
sb.append(data[i]);
if((i+1)%data.length==rear){ //当遍历到指针i下一个位置是尾指针处时,此时i在最后一个元素处,打印']'
sb.append(']');
}else{ //否则在元素间打印','
sb.append(',');
}
}
}
return sb.toString(); //返回sb对象,调用toString方法
}
@Override
public Iterator<E> iterator() {
return new ArrayQueueLoopIterator(); //创建内部类对象
}
//迭代器 内部类(就是打印表内元素,并让其支持foreach循环)
private class ArrayQueueLoopIterator implements Iterator{
int p=front; //从头指针处开始
@Override
public boolean hasNext() {
return p!=rear; //继续条件:p指针不指向尾指针处
}
@Override
public Object next() {
E ret=data[p]; //ret输出元素
p=(p+1)%data.length; //p递增
return ret;
}
}
}
写一个测试类,写完每个方法时都可以测试是否有错误,避免全部写完后难以找到BUG。
来源:CSDN
作者:~inspire
链接:https://blog.csdn.net/qq_43624033/article/details/103572549