<数据结构系列3>队列的实现与变形(循环队列)

数据结构第三课了,今天我们再介绍一种很常见的线性表——队列

就像它的名字,队列这种数据结构就如同生活中的排队一样,队首出队,队尾进队。以下一段是百度百科中对队列的解释:

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。

队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。

 队列主要分为顺序队列以及循环队列,在这我两种队列都会实现一下, 并在最后进行一下性能上的测试比较。

ok,让我们开始今天的学习吧。

 

首先,还是先创建一个队列的接口:

public interface Queue<E> {
    int getSize();    //得到队列元素个数
    boolean isEmpty();    //判断队列是否为空
    void enqueue(E e);    //入队
    E dequeue();    //出队
    E getFront();    //查看队首元素
}

 

由于在<数据结构系列1>(https://www.cnblogs.com/LASloner/p/10721407.html)中我们已经实现了ArrayList,所以今天我们依旧就用Array来作为Queue的数据存储方式吧:

public class ArrayQueue<E> implements Queue<E> {
    //这里用的是自己实现的Array,当然读者可以直接用Java自带的ArrayList,效果相同
    private Array<E> array=new Array<>();
    
    //有参构造
    public ArrayQueue(int capacity) {
        array=new Array<>(capacity);
    }
    //无参构造
    public ArrayQueue() {
        array=new Array<>();
    }
    
    //获取队列元素个数
    @Override
    public int getSize() {
        return array.getSize();
    }

    //判断队列是否为空
    @Override
    public boolean isEmpty() {
        return array.isEmpty();
    }

    //获得队列的容量
    public int getCapacity(){
        return array.getCapacity();
    }
    
    @Override
    public void enqueue(E e) {
        array.addLast(e);
    }

    @Override
    public E dequeue() {
        return array.removeFirst();
    }

    @Override
    public E getFront() {
        return array.get(0);
    }
@Override
public String toString() { StringBuilder res=new StringBuilder(); res.append("Queue: "); res.append("front ["); for(int i = 0 ; i < array.getSize() ; i ++){ res.append(array.get(i)); if(i != array.getSize() - 1) res.append(", "); } res.append("] rear"); return res.toString(); } }

 

 这并不是一件难事吧,那再让我们测试一下吧:

    public static void main(String[] args) {
     ArrayQueue<Integer> queue = new ArrayQueue<>();
        for(int i = 0 ; i < 10 ; i ++){
            queue.enqueue(i);
            System.out.println("入队:"+queue);
            if(i % 3 == 2){
                queue.dequeue();
                System.out.println("出队:"+queue);
            }
        }
    }

 

输出如下: 

入队:Queue: front [0] rear
入队:Queue: front [0, 1] rear
入队:Queue: front [0, 1, 2] rear
出队:Queue: front [1, 2] rear
入队:Queue: front [1, 2, 3] rear
入队:Queue: front [1, 2, 3, 4] rear
入队:Queue: front [1, 2, 3, 4, 5] rear
出队:Queue: front [2, 3, 4, 5] rear
入队:Queue: front [2, 3, 4, 5, 6] rear
入队:Queue: front [2, 3, 4, 5, 6, 7] rear
入队:Queue: front [2, 3, 4, 5, 6, 7, 8] rear
出队:Queue: front [3, 4, 5, 6, 7, 8] rear
入队:Queue: front [3, 4, 5, 6, 7, 8, 9] rear

 

这样就算是实现了顺序队列了,但是呢,由于顺序队列存储数据的方式使用了之前封装好的ArrayList,并不能很好的体现队列的这种数据结构,都没使用到front和rear,接下来,我们再实现一下循环队列,以此更好的了解队列。

那之前先看看什么是循环队列:

在实际使用队列时,为了使队列空间能重复使用,往往对队列的使用方法稍加改进:无论插入或删除,一旦rear指针增1或front指针增1 时超出了所分配的队列空间,就让它指向这片连续空间的起始位置。自己真从MaxSize-1增1变到0,可用取模运算rear%MaxSize和front%MaxSize来实现。这实际上是把队列空间想象成一个环形空间,环形空间中的存储单元循环使用,用这种方法管理的队列也就称为循环队列。除了一些简单应用之外,真正实用的队列是循环队列。  
在循环队列中,当队列为空时,有front=rear,而当所有队列空间全占满时,也有front=rear。为了区别这两种情况,规定循环队列最多只能有MaxSize-1个队列元素,当循环队列中只剩下一个空存储单元时,队列就已经满了。因此,队列判空的条件时front=rear,而队列判满的条件时front=(rear+1)%MaxSize。

 

依旧需要实现Queue接口:

 

public class LoopQueue<E> implements Queue<E>{
    private E[] data;
    private int front;//队首
    private int rear;//队尾
    private int size;
    
    //有参构造
    public LoopQueue(int capacity) {
        data =(E[])new Object[capacity+1];//浪费一个空间用作判断 front==(rear+1)%(capacity+1)时为满
        front=0;
        rear=0;
        size=0;
    }
    //无参构造
    public LoopQueue() {
        this(10);
    }
    
    //返回队列中元素的个数
    @Override
    public int getSize() {
        return size;
    }

    //判断队列是否为空
    @Override
    public boolean isEmpty() {
        return front==rear;
    }

    //返回队列容量
    public int getCapacity(){
        return data.length - 1;
    }
    
    //入队
    @Override
    public void enqueue(E e) {
        if((rear+1)%data.length==front)//当队列满了之后扩容
            resize(getCapacity()*2);
        data[rear]=e;
        rear=(rear+1)%data.length;//这里的data.length就代表Maxsize
        size++;
    }

    //出队
    @Override
    public E dequeue() {
        if(isEmpty())
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
        E res=data[front];
        data[front]=null;
        front=(front+1)%data.length;
        size--;
        if(size==getCapacity()/4&&getCapacity()/2!=0)//动态缩容
            resize(getCapacity()/2);
        return res;
    }
    
    //获取队首元素
    @Override
    public E getFront() {
        if(isEmpty())
            throw new IllegalArgumentException("Queue is empty.");
        return data[front];
    }
    
    //动态扩容
    private void resize(int newCapacity) {
        E[] newData=(E[])new Object[newCapacity+1];
        for(int i=0;i<size;i++) {
            newData[i]=data[(front+i)%data.length];
        }
        data=newData;
        front=0;
        rear=size;
    }
    
    
    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append(String.format("Queue: size = %d , capacity = %d\t", size, getCapacity()));
        res.append("front [");
        for(int i = front ; i != rear ; i = (i + 1) % data.length){
            res.append(data[i]);
            if((i + 1) % data.length != rear)
                res.append(", ");
        }
        res.append("] rear");
        return res.toString();
    }
    
}

 

再写一个Main方法测试一下: 

public static void main(String[] args) {
        LoopQueue<Integer> queue = new LoopQueue<>();
        for(int i = 0 ; i < 10 ; i ++){
            queue.enqueue(i);
            System.out.println("入队:"+queue);

            if(i % 3 == 2){
                queue.dequeue();
                System.out.println("出队:"+queue);
            }
        }
    }

 

 输出:

入队:Queue: size = 1 , capacity = 10    front [0] rear
入队:Queue: size = 2 , capacity = 10    front [0, 1] rear
入队:Queue: size = 3 , capacity = 10    front [0, 1, 2] rear
出队:Queue: size = 2 , capacity = 5    front [1, 2] rear
入队:Queue: size = 3 , capacity = 5    front [1, 2, 3] rear
入队:Queue: size = 4 , capacity = 5    front [1, 2, 3, 4] rear
入队:Queue: size = 5 , capacity = 5    front [1, 2, 3, 4, 5] rear
出队:Queue: size = 4 , capacity = 5    front [2, 3, 4, 5] rear
入队:Queue: size = 5 , capacity = 5    front [2, 3, 4, 5, 6] rear
入队:Queue: size = 6 , capacity = 10    front [2, 3, 4, 5, 6, 7] rear
入队:Queue: size = 7 , capacity = 10    front [2, 3, 4, 5, 6, 7, 8] rear
出队:Queue: size = 6 , capacity = 10    front [3, 4, 5, 6, 7, 8] rear
入队:Queue: size = 7 , capacity = 10    front [3, 4, 5, 6, 7, 8, 9] rear

 

 那现在,循环队列相关的代码就全部完成了,需要注意的是,我们实现的循环队列是可以动态扩容的,实际上我实现的所有的线性表都是动态扩容的,希望读者注意。

既然实现了两种队列,就自然的做一下比较吧,首先做一下性能比较:

测试函数如下:

public class PerformanceTest {
    private static double queueTest(Queue<Integer> q,int opCount) {
        long startTime = System.nanoTime();//当前毫秒值

        Random random = new Random();
        for(int i = 0 ; i < opCount ; i ++)
            q.enqueue(random.nextInt(Integer.MAX_VALUE));
        for(int i = 0 ; i < opCount ; i ++)
            q.dequeue();

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0;
    }
    
    public static void main(String[] args) {
        int opCount = 100000;

        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
        double time1 = queueTest(arrayQueue, opCount);
        System.out.println("ArrayQueue, time: " + time1 + " s");

        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        double time2 = queueTest(loopQueue, opCount);
        System.out.println("LoopQueue, time: " + time2 + " s");    
    }
}

输出为:

  ArrayQueue, time: 6.924613333 s
  LoopQueue, time: 0.020527111 s

 

再看时间复杂度的比较:

ArrayQueue<E>
void enqueue(E)    O(1)
E dequeue()           O(n)
E front()                O(1)
int getSize()        O(1)
boolean isEmpty     O(1)

LoopQueue<E>
void enqueue(E)    O(1)
E dequeue()           O(1)
E front()                O(1)
int getSize()        O(1)
boolean isEmpty     O(1)

 

经过对比不难看出,循环队列的效率要高于顺序队列,但是,也不得不说,循环队列在实现上要复杂一些。

 

 哈哈,真是难得,竟然一天内更了两篇博文,继续加油!!

 

对于学习,四个字概括:至死方休

 

转载于:https://www.cnblogs.com/LASloner/p/10834507.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值