数据结构之——队列与循环队列
数据结构学习之——队列与循环队列
- 什么是队列(Queue)
- 队列基于动态数组的实现及时间复杂度分析
- 优化队列
- 循环队列(LoopQueue)
什么是队列(Queue)
队列(Queue)同栈(stack)一样也是一种运算收限的线性数据结构,参考:数据结构之——栈。栈即:LIFO(Last In First Out),队列则是FIFO(First In First Out),也就是说队列这种数据结构仅允许在一端(队尾)添加元素,在另一端(队首)删除元素,所以说队列是一种先进先出的数据结构。可以将队列想象成银行柜台的排队机制一样,在前面排队的人可以先办理业务,在最后排队的人等到前面所有的人办理完毕后,才可以进行业务的处理,如图:
队列基于动态数组的实现及时间复杂度分析
队列同ArrayStack的实现一样,都需要基于动态数组作为底层实现。
动态数组实现代码,动态数组实现原理
在设计模式上,同ArrayStack一样,设计的是Queue这样一个接口,并创建ArrayQueue这样一个类implements这个接口,Queue接口的方法与Stack栈的方法大体相同,只不过我们将入栈push设计成enqueue(入队),出栈pop设计为dequeue(出队)。接口代码如下:
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<>();}@Overridepublic int getSize(){return array.getSize();}@Overridepublic boolean isEmpty(){return array.isEmpty();}public int getCapacity(){return array.getCapacity();}@Overridepublic void enqueue(E e){array.addLast(e);}@Overridepublic E dequeue(){return array.removeFirst();}@Overridepublic E getFront(){if(array.isEmpty)throw new IllegalArgumentException("ArrayQueue is Empty");return array.get(0)}@Overridepublic 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)级别的算法。
E dequeue(); 复制代码dequeue()操作相当于动态数组的removeFirst()操作,在数组的头部删除一个元素,array[0] 后面的所有元素都需要向前挪动一个位置,所以dequeue出队是一个O(n)级别的算法。
综上分析,ArrayQueue还是有些不完美的地方,ArrayStack所有的操作均为O(1)级别的算法,但是基于动态数组实现的队列ArrayQueue 在出队操作dequeue上性能还是略显不足,LoopQueue(循环队列)就优化了ArrayQueue出队这样一个问题。
优化ArrayQueue
我们已经知道了,ArrayQueue美中不足的地方就在于dequeue这样一个操作是O(n)级别的算法。出现这个问题的原因实际上是因为每次进行出队操作时,动态数组都需要将array[0]后面所有的元素向前挪动一个单位,但实际上想一想这个过程并不“划算”,因为,队列元素的数量达到万以上的级别时,仅仅删除一个元素,我们就需要将所有的元素进行一次大换血。和银行柜台业务办理的排队不同,银行柜台的一号办理人办理完毕,后面所有的人只需要上前一小步就可以了,但是对于计算机来讲,每一个数据的调整都需要计算机亲历躬行。有什么办法可以避免这种大规模的动辄呢?我们可以使用两个变量去维护队列的队首和队尾。
示例:enqueue元素“D”
示例:dequeue
上述思路优化后的队列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);}@Overridepublic int getSize(){return tail-front;}@Overridepublic boolean isEmpty(){return front==tail;}@Overridepublic 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;}@Overridepublic void enqueue(E e){if(tail == data.length-1)resize((tail-front)*2);data[tail] = e;tail++;}@Overridepublic E dequeue(){E ret = data[front];// Loitering Objectdata[front] = null;front++;if(((tail-front)==(data.length-1)/4) && (data.length-1)/2!=0)resize((data.length-1)/2);return ret;}@Overridepublic 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时进行“扩容”操作,这里面扩容二字我使用了双引号,因为有可能这个“扩容”实际上是缩容。
我们规定了enqueue操作中, if(tail == data.length-1)resize((tail-front)*2); 复制代码
在上图的示例中,如果reisze,我们实际上就相当于进行了缩容的操作。我们这样的设计看起来解决了问题,但仍然不灵活,我们希望在入队时的resize只涉及到扩容,出队时的resize只涉及缩容,我们是否能对这样的需求进行优化呢?
循环队列(LoopQueue)
循环队列的思想和ArrayQueue优化后的MyQueue大体相同,只不过循环队列里面加入了更加巧妙的循环机制。
上例中,我们规定tail == data.length-1队列为满进行resize,但是这一次我们换一种思路。当继续向当前队列添加元素时,我们这样做:
变量tail重新回到了起点,这也就是循环队列称之为“循环”的意义所在。那么什么时候表示当前队列已满需要进行resize呢?
当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);}@Overridepublic int getSize(){if(tail<front)return data.length-(front-tail);else{return tail-front;}}public int getCapacity(){return data.length-1;}@Overridepublic 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;}@Overridepublic void enqueue(E e){if(front==(tail+1)%data.length)resize(2*getSize());data[tail] = e;tail = (tail+1)%data.length;}@Overridepublic E dequeue(){E ret = data[front];// Loitering Objectdata[front] = null;front = (front+1)%data.length;if(getSize() == getCapacity()/4 && getCapacity()/2!=0){resize(getCapacity()/2);}return ret;}@Overridepublic E getFront(){if(getSize()==0)throw new IllegalArgumentException("LoopQueue is empty");return data[front];}@Overridepublic 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的环境下测试结果为:
导致两者之间造成巨大差异的结果就是dequeue操作,ArrayQueue的dequeue操作为O(n)级别的算法,而LoopQueue的dequeue操作 在均摊的时间复杂度上为O(1)。
总结
以上是生活随笔为你收集整理的数据结构之——队列与循环队列的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: Dubbo快速启动示例
- 下一篇: shell脚本将本地docker镜像pu