内容简介:数据结构学习之——队列与循环队列队列(Queue)同栈(stack)一样也是一种运算收限的线性数据结构,参考:数据结构之——栈。栈即:LIFO(Last In First Out),队列则是FIFO(First In First Out),也就是说队列这种数据结构仅允许在一端(队尾)添加元素,在另一端(队首)删除元素,所以说队列是一种先进先出的数据结构。可以将队列想象成银行柜台的排队机制一样,在前面排队的人可以先办理业务,在最后排队的人等到前面所有的人办理完毕后,才可以进行业务的处理,如图:队列同Array
数据结构学习之——队列与循环队列
- 什么是队列(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(出队)。接口代码如下:
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)级别的算法。
E dequeue(); 复制代码
dequeue()操作相当于动态数组的removeFirst()操作,在数组的头部删除一个元素,array[0] 后面的所有元素都需要向前挪动一个位置,所以dequeue出队是一个O(n)级别的算法。
综上分析,ArrayQueue还是有些不完美的地方,ArrayStack所有的操作均为O(1)级别的算法,但是基于动态数组实现的队列ArrayQueue 在出队操作dequeue上性能还是略显不足,LoopQueue(循环队列)就优化了ArrayQueue出队这样一个问题。
优化ArrayQueue
我们已经知道了,ArrayQueue美中不足的地方就在于dequeue这样一个操作是O(n)级别的算法。出现这个问题的原因实际上是因为每次进行出队操作时,动态数组都需要将array[0]后面所有的元素向前挪动一个单位,但实际上想一想这个过程并不“划算”,因为,队列元素的数量达到万以上的级别时,仅仅删除一个元素,我们就需要将所有的元素进行一次大换血。和银行柜台业务办理的排队不同,银行柜台的一号办理人办理完毕,后面所有的人只需要上前一小步就可以了,但是对于计算机来讲,每一个数据的调整都需要计算机亲历躬行。有什么办法可以避免这种大规模的动辄呢?我们可以使用两个变量去维护队列的队首和队尾。
假设变量为front和tail。front维护队首,变量tail维护队尾,当front==tail时,队列为空,每增加一个元素,tail就像后移动一个单位,所以我们需要多使用一个空间去维护 tail这样一个变量,这样我们在设计模式上就需要开辟用户指定的capacity加1的数组空间。对于用户来讲,capacity==n,但实际上真正的capacity也就是data.length==n+1,因为我们需要多浪费一个空间去维护tail这个变量,但是浪费这样一个空间相比于dequeue的大换血O(n)操作也是值得的。
示例:enqueue元素“D”上述思路优化后的队列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时进行“扩容”操作,这里面扩容二字我使用了双引号,因为有可能这个“扩容”实际上是缩容。
我们规定了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); } @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的环境下测试结果为:
导致两者之间造成巨大差异的结果就是dequeue操作,ArrayQueue的dequeue操作为O(n)级别的算法,而LoopQueue的dequeue操作 在均摊的时间复杂度上为O(1)。
以上所述就是小编给大家介绍的《数据结构之——队列与循环队列》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Web Designer's Idea Book
Patrick Mcneil / How / 2008-10-6 / USD 25.00
The Web Designer's Idea Book includes more than 700 websites arranged thematically, so you can find inspiration for layout, color, style and more. Author Patrick McNeil has cataloged more than 5,000 s......一起来看看 《The Web Designer's Idea Book》 这本书的介绍吧!