数据结构之——队列与循环队列

数据结构学习之——队列与循环队列java

  • 什么是队列(Queue)
  • 队列基于动态数组的实现及时间复杂度分析
  • 优化队列
  • 循环队列(LoopQueue)

什么是队列(Queue)

队列(Queue)同栈(stack)同样也是一种运算收限的线性数据结构,参考:数据结构之——栈。栈即:LIFO(Last In First Out),队列则是FIFO(First In First Out),也就是说队列这种数据结构仅容许在一端(队尾)添加元素,在另外一端(队首)删除元素,因此说队列是一种先进先出的数据结构。能够将队列想象成银行柜台的排队机制同样,在前面排队的人能够先办理业务,在最后排队的人等到前面全部的人办理完毕后,才能够进行业务的处理,如图:
git

bank

队列基于动态数组的实现及时间复杂度分析

队列同ArrayStack的实现同样,都须要基于动态数组做为底层实现。
动态数组实现代码,动态数组实现原理
在设计模式上,同ArrayStack同样,设计的是Queue这样一个接口,并建立ArrayQueue这样一个类implements这个接口,Queue接口的方法与Stack栈的方法大致相同,只不过咱们将入栈push设计成enqueue(入队),出栈pop设计为dequeue(出队)。接口代码以下:github

public interface Queue<E>{
    void enqueue(E e);
    E dequeue();
    E getFront();
    int getSize();
    boolean is Empty();
}
复制代码

ArrayQueue代码以下:算法

public class ArrayQueue<E> implements Queue<E>{
    private Array<E> 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(){
        if(array.isEmpty)
            throw new IllegalArgumentException("ArrayQueue is Empty");
        return array.get(0)
    }
    
    @Override
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("Queue:\n");
        sb.append("front:[");
        for(int i=0;i<getSize();i++){
            sb.append(array.get(i));
            if(i!=getSize()-1){
                sb.append(",");
            }else{
                sb.append("]tail");
            }
        }
        return sb.toString();
    }
}
复制代码

时间复杂度分析以下:设计模式

E getFront();
 int getSize();
 boolean is Empty();
复制代码

以上方法,时间复杂度为:O(1)。数组

void enqueue(E e);
复制代码

由于入队操做至关于在动态数组的尾部添加元素,虽然有resize()这样一个O(n)级别的算法,可是以均摊时间复杂度分析,enqueue操做仍然是一个O(1)级别的算法。bash

E dequeue();
复制代码

dequeue()操做至关于动态数组的removeFirst()操做,在数组的头部删除一个元素,array[0] 后面的全部元素都须要向前挪动一个位置,因此dequeue出队是一个O(n)级别的算法。
综上分析,ArrayQueue仍是有些不完美的地方,ArrayStack全部的操做均为O(1)级别的算法,可是基于动态数组实现的队列ArrayQueue 在出队操做dequeue上性能仍是略显不足,LoopQueue(循环队列)就优化了ArrayQueue出队这样一个问题。数据结构

优化ArrayQueue

咱们已经知道了,ArrayQueue美中不足的地方就在于dequeue这样一个操做是O(n)级别的算法。出现这个问题的缘由其实是由于每次进行出队操做时,动态数组都须要将array[0]后面全部的元素向前挪动一个单位,但实际上想想这个过程并不“划算”,由于,队列元素的数量达到万以上的级别时,仅仅删除一个元素,咱们就须要将全部的元素进行一次大换血。和银行柜台业务办理的排队不一样,银行柜台的一号办理人办理完毕,后面全部的人只须要上前一小步就能够了,可是对于计算机来说,每个数据的调整都须要计算机亲历躬行。有什么办法能够避免这种大规模的动辄呢?咱们可使用两个变量去维护队列的队首和队尾。
app

myQueue
假设变量为front和tail。front维护队首,变量tail维护队尾,当front==tail时,队列为空,每增长一个元素,tail就像后移动一个单位,因此咱们须要多使用一个空间去维护 tail这样一个变量,这样咱们在设计模式上就须要开辟用户指定的capacity加1的数组空间。对于用户来说,capacity==n,但实际上真正的capacity也就是data.length==n+1,由于咱们须要多浪费一个空间去维护tail这个变量,可是浪费这样一个空间相比于dequeue的大换血O(n)操做也是值得的。
示例:enqueue元素“D”

myQueue2

示例:dequeue

myQueue3

上述思路优化后的队列MyQueue代码以下:

public class MyQueue<E> implements Queue<E> {
    private int front;
    private int tail;
    private E[]data;
    public MyQueue(int capacity){
        data = (E[])new Object[capacity+1];
    }
    public MyQueue(){
        this(10);
    }
    @Override
    public int getSize(){
        return tail-front;
    }
    @Override
    public boolean isEmpty(){
        return front==tail;
    }
    @Override
    public E getFront(){
        return data[front];
    }
    private void resize(int newCapacity){
        E[]newData = (E[])new Object[newCapacity+1];
        for(int i=0;i<(tail-front);i++){
            newData[i] = data[front+i];
        }
        data = newData;
        tail = tail - front;
        front = 0;
    }
    @Override
    public void enqueue(E e){
        if(tail == data.length-1)
            resize((tail-front)*2);

        data[tail] = e;
        tail++;
    }

    @Override
    public E dequeue(){
        E ret = data[front];
        // Loitering Object
        data[front] = null;
        front++;
        if(((tail-front)==(data.length-1)/4) && (data.length-1)/2!=0)
            resize((data.length-1)/2);

        return ret;
    }

    @Override
    public String toString(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("MyQueue size=%d,capacity=%d\n",tail-front,data.length-1));
        stringBuilder.append("front:[");
        for(int i=front;i<tail;i++){
            stringBuilder.append(data[i]);
            if((i+1)!=tail){
                stringBuilder.append(",");
            }
        }
        stringBuilder.append("]tail");
        return stringBuilder.toString();
    }
}

复制代码

MyQueue对ArrayQueue进行了优化操做,本来dequeue须要O(n)的时间复杂度,进行优化后,dequeue由O(n)的时间复杂度变为了均摊O(1)的时间复杂度。这里面须要注意的是:MyQueue的enqueue入队操做规定了tail == data.length-1时进行“扩容”操做,这里面扩容二字我使用了双引号,由于有可能这个“扩容”其实是缩容。
dom

myQueue4

咱们规定了enqueue操做中,

if(tail == data.length-1)
    resize((tail-front)*2);
复制代码

在上图的示例中,若是reisze,咱们实际上就至关于进行了缩容的操做。咱们这样的设计看起来解决了问题,但仍然不灵活,咱们但愿在入队时的resize只涉及到扩容,出队时的resize只涉及缩容,咱们是否能对这样的需求进行优化呢?

循环队列(LoopQueue)

循环队列的思想和ArrayQueue优化后的MyQueue大致相同,只不过循环队列里面加入了更加巧妙的循环机制。

myQueue4

上例中,咱们规定tail == data.length-1队列为满进行resize,可是这一次咱们换一种思路。当继续向当前队列添加元素时,咱们这样作:

loopQueue

变量tail从新回到了起点,这也就是循环队列称之为“循环”的意义所在。那么何时表示当前队列已满须要进行resize呢?

loopQueue3

loopQueue2

当front == (tail+1)%data.length,当这个条件成立时,也就说明了队列为满,须要进行扩容操做了。循环队列实现代码以下:

public class LoopQueue<E> implements Queue<E> {
    private E[]data;
    private int front;
    private int tail;
    public LoopQueue(int capacity){
        data = (E[])new Object[capacity+1];
    }
    public LoopQueue(){
        this(10);
    }
    @Override
    public int getSize(){
        if(tail<front)
            return data.length-(front-tail);
        else{
            return tail-front;
        }
    }

    public int getCapacity(){
        return data.length-1;
    }
    @Override
    public boolean isEmpty(){
        return getSize()==0;
    }

    private void resize(int newCapacity){
        E[]newData = (E[])new Object[newCapacity+1];
        for(int i=0;i<getSize();i++){
            newData[i] = data[(i+front)%data.length];
        }
        data = newData;
        tail = getSize();
        front = 0;
    }
    @Override
    public void enqueue(E e){
        if(front==(tail+1)%data.length)
            resize(2*getSize());

        data[tail] = e;
        tail = (tail+1)%data.length;
    }

    @Override
    public E dequeue(){
        E ret = data[front];
        // Loitering Object
        data[front] = null;
        front = (front+1)%data.length;
        if(getSize() == getCapacity()/4 && getCapacity()/2!=0){
            resize(getCapacity()/2);
        }
        return ret;
    }

    @Override
    public E getFront(){
        if(getSize()==0)
            throw new IllegalArgumentException("LoopQueue is empty");

        return data[front];
    }

    @Override
    public String toString(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("LoopQueue size:%d,capacity:%d\n",getSize(),getCapacity()));
        stringBuilder.append("front:[");
        for(int i=front;i!=tail;i=(i+1)%data.length){
            stringBuilder.append(data[i]);
            if((i+1)%data.length!=tail)
                stringBuilder.append(",");
        }
        stringBuilder.append("]tail");
        return stringBuilder.toString();
    }
}

复制代码

如今咱们对ArrayQueue,LoopQueue进行性能上的测试,

import java.util.Random;

public class Main {
    private static double testQueue(Queue<Integer>q,int testCount){
        long startTime = System.nanoTime();
        Random random = new Random();
        for(int i=0;i<testCount;i++)
            q.enqueue(random.nextInt(Integer.MAX_VALUE));

        for(int i=0;i<testCount;i++)
            q.dequeue();

        long endTime = System.nanoTime();
        return (endTime-startTime)/1000000000.0;
    }
    public static void main(String[]args){
        int testCount = 100000;
        ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
        LoopQueue<Integer> loopQueue = new LoopQueue<>();
        double loopQueueTime = testQueue(loopQueue,testCount);
        double arrayQueueTime = testQueue(arrayQueue,testCount);
        System.out.println("LoopQueue:"+loopQueueTime);
        System.out.println("ArrayQueue"+arrayQueueTime);
    }
}

复制代码

在jdk1.8的环境下测试结果为:

loopQueueTest
致使二者之间形成巨大差别的结果就是dequeue操做,ArrayQueue的dequeue操做为O(n)级别的算法,而LoopQueue的dequeue操做 在均摊的时间复杂度上为O(1)。
相关文章
相关标签/搜索