1. 程式人生 > 實用技巧 >佇列(Queue)

佇列(Queue)

在“佇列”(Queue)這種資料結構中,資料項是先進先出(FIFO:first in first out)。佇列的容量可以有限,也可以是無限的。

一、基於陣列的Queue實現

一般情況下,對於Queue而言,最核心的操作是:插入佇列(enqueue)、移出佇列(dequeue)。因為在佇列中,插入操作是插入到佇列的最後,而移出操作是移出佇列的頭部元素。因此我們通常會使用兩個變數front(隊頭指標)和rear(隊尾指標)記錄當前元素的位置。

假設我們有一個容量有限佇列,用於存放字母,如下圖所示:


當我們要插入一個元素時,因為總是插入到佇列的最尾部,所以插入的位置是rear+1的位置。

當我們要移出一個元素時,是從隊頭指標front的位置開始移除(因為Queue頭部的元素是最先加入進來的,根據FIFO原則,應該最先移除)。當移除一個元素之後,front應該加1,因為移出一個元素之後,下一個元素就變成了第一個元素。例如現在我們移出了5個元素:

當移出5個元素之後,隊頭指標就移動到了字母F的位置,前五個位置空下來了。隊尾指標rear不變。

現在問題來了:佇列頭部移出的幾個元素,位置空下來了。當我們新增元素的時候,從隊尾開始加,當新增到最後一個位置時,怎麼辦?不讓新增時不合理的,畢竟前幾個位置已經空下來了。我們的期望是,當一個位置上的元素被移出之後,這個位置是可以被重複使用的。而我們這裡討論的是有限容量的資料,如果我們還繼續新增元素,那麼就要往佇列頭部來新增。

這實際上就涉及到了迴圈佇列的概念。也就是當隊尾指標到了陣列的最後一個下標時,下一個位置應該就是陣列的首部。
因此,當隊尾指標指向陣列頂端的時候,我們要將隊尾指標(rear)重置為-1,此時再加1,就是0,也就是陣列頂端例。如我們又添加了5個字母,那麼結果應該如下圖:


程式碼實現:

  1. packagecom.tianshouzhi.algrithm.queue;
  2. importjava.util.Arrays;
  3. /**
  4. *佇列滿足的條件是FIFO先進先出,本例是基於陣列完成的Queue
  5. *
  6. *@authorAdministrator
  7. *
  8. */
  9. publicclassArrayQueue<T>{
  10. Object[]data=null;
  11. //對頭指標
  12. privateintfront;
  13. //隊尾指標
  14. privateintrear;
  15. //佇列中當前的元素
  16. privateintitemNums;
  17. privateintmaxSize;
  18. publicArrayQueue(IntegermaxSize){
  19. this.maxSize=maxSize;
  20. data=newObject[maxSize];
  21. front=0;
  22. rear=-1;
  23. itemNums=0;
  24. }
  25. /**
  26. *插入元素:
  27. *1、一般情況下,插入操作是在佇列不滿的情況下,才呼叫。因此在插入前,應該先呼叫isFull
  28. *2、在佇列中插入元素,正常情況下是在隊尾指標(rear)+1的位置上插入,因為我們編寫的是迴圈佇列
  29. *因此,當隊尾指標指向陣列頂端的時候,我們要將隊尾指標(rear)重置為-1,此時再加1,就是0,也就是陣列頂端
  30. *@paramelement
  31. */
  32. publicvoidenqueue(Telement){
  33. if(isFull()){
  34. thrownewIllegalStateException("queueisfull!");
  35. }
  36. needCycle();
  37. data[++rear]=element;
  38. itemNums++;
  39. }
  40. /**
  41. *讓佇列支援迴圈的核心程式碼:
  42. *如果rear=maxSize-1,說明下一個元素因該是的陣列的首部,將rear置為-1
  43. *因為插入操作是隊尾指標rear+1的位置,因此下一個位置就是0,即陣列第一個元素下標
  44. */
  45. privatevoidneedCycle(){
  46. if(rear==maxSize-1){
  47. rear=-1;
  48. }
  49. }
  50. /**
  51. *移除元素,返回隊頭指標front所指向的資料項的值
  52. *正常情況下,在remove之前,應該呼叫isEmpty,如果為空,則不能輸入
  53. *@return
  54. */
  55. @SuppressWarnings("unchecked")
  56. publicTdequeue(){
  57. if(isEmpty()){
  58. thrownewIllegalStateException("noelementsinthequeue");
  59. }
  60. Tt=(T)data[front];
  61. data[front]=null;
  62. front=front+1;
  63. if(front==maxSize){
  64. front=0;
  65. }
  66. itemNums--;
  67. returnt;
  68. }
  69. /**
  70. *檢視佇列首部元素,不移除
  71. *
  72. *@return
  73. */
  74. @SuppressWarnings("unchecked")
  75. publicTpeekFront(){
  76. if(isEmpty()){
  77. thrownewIllegalStateException("noelementsinthequeue");
  78. }
  79. return(T)data[front];
  80. }
  81. publicbooleanisEmpty(){
  82. returnitemNums==0;
  83. }
  84. publicbooleanisFull(){
  85. returnitemNums==maxSize;
  86. }
  87. publicintsize(){
  88. returnitemNums;
  89. }
  90. publicintgetMaxSize(){
  91. returnmaxSize;
  92. }
  93. publicvoidsetMaxSize(intmaxSize){
  94. this.maxSize=maxSize;
  95. }
  96. @Override
  97. publicStringtoString(){
  98. return"ArrayQueue[container="+Arrays.toString(data)
  99. +",front="+front+",rear="+rear+",size="
  100. +itemNums+",maxSize="+maxSize+"]";
  101. }
  102. }

測試:

  1. packagecom.tianshouzhi.algrithm.queue;
  2. importorg.junit.Test;
  3. /**
  4. *本測試演示上述分析插入字母案例,因為A-P共有16個字母。而16個字母插滿之後,還有3個空餘位置,說明佇列大小為19
  5. *因此在每個測試方法中,我們都設定佇列初始大小為16
  6. *@authorAdministrator
  7. *
  8. */
  9. publicclassArrayQueueTest{
  10. privatecharbegin='A';
  11. privatecharend='P';
  12. /**
  13. *佇列中插入16個元素A-P,觀察front、rear指標的位置
  14. *一般情況下,插入操作是在佇列不滿的情況下,才呼叫。因此在插入前,應該先呼叫isFull()
  15. *如果佇列中元素已經滿了,就不應該繼續插入
  16. */
  17. @Test
  18. publicvoidtestInsertA_P(){
  19. ArrayQueue<Character>queue=newArrayQueue<Character>(19);
  20. for(chari=begin;i<=end;i++){
  21. if(!queue.isFull()){
  22. queue.enqueue(i);
  23. }
  24. }
  25. System.out.println(queue);
  26. }
  27. /**
  28. *測試新增之後,再刪除
  29. */
  30. @Test
  31. publicvoidtestInsertRemoveInsert(){
  32. //初始資料,佇列中有5個元素
  33. charcurrent=0;
  34. ArrayQueue<Character>queue=newArrayQueue<Character>(19);
  35. for(chari=begin;i<=end;i++){
  36. if(!queue.isFull()){
  37. queue.enqueue(i);
  38. current=i;
  39. }
  40. }
  41. System.out.println(queue);
  42. System.out.println("初始資料:\n"+queue);
  43. //移除3個元素
  44. System.out.println("移除佇列首部的5個元素:");
  45. for(inti=0;i<5;i++){
  46. if(!queue.isEmpty()){
  47. queue.dequeue();
  48. }
  49. }
  50. System.out.println(queue);
  51. System.out.println("新增元素5個元素,陣列尾部只剩3個位置,因此有個元素要新增到佇列首部:");
  52. intendChar=current+5;
  53. for(;current<=endChar;current++){
  54. if(!queue.isFull()){
  55. queue.enqueue(current);
  56. }
  57. }
  58. System.out.println(queue);
  59. }
  60. }

二、Java中的Queue

java中定義了一個java.util.Queue介面,定義瞭如下方法:

  1. publicinterfaceQueue<E>extendsCollection<E>{
  2. //增加一個元索到隊尾,如果佇列已滿,則丟擲一個IIIegaISlabEepeplian異常
  3. booleanadd(Ee);
  4. //移除並返回佇列頭部的元素,如果佇列為空,則丟擲一個NoSuchElementException異常
  5. Eremove();
  6. //新增一個元素到隊尾,如果佇列已滿,則返回false
  7. booleanoffer(Ee);
  8. //移除並返問佇列頭部的元素,如果佇列為空,則返回null
  9. Epoll();
  10. //返回佇列頭部的元素,如果佇列為空,則返回null
  11. Epeek();
  12. //返回佇列頭部的元素,如果佇列為空,則丟擲一個NoSuchElementException異常
  13. Eelement();
  14. }

可以發現這些方法都是兩兩成對的。

Queue還有一個子介面BlockingQueue,主要定義了一些在併發環境下,方法應該具有特性。

  1. publicinterfaceBlockingQueue<E>extendsQueue<E>{
  2. ...
  3. //新增一個元素,如果佇列滿,則阻塞
  4. voidput(Ee)throwsInterruptedException;
  5. //移除並返回佇列頭部的元素,如果佇列為空,則阻塞
  6. Etake()throwsInterruptedException;
  7. ...
  8. }

Java中還提供了一個java.util.Deque雙端佇列(deque,全名double-ended queue),就是佇列中的元素可以從兩端彈出,其限定插入和刪除操作在表的兩端進行。

這個我們就不細說了,因為在實際開發中,Deque遠遠沒有Queue實用。

前面我們講解的Queue是基於陣列實現了,實際上,Queue也可以基於連結串列(LinkedList)實現,例如新增元素的時候總是呼叫addFisrt方法,移除元素的時候總是呼叫removeLast方法,則實現了FIFO的功能,因此連結串列是可以實現Queue的功能的,這也是我們在前面說連結串列是除了陣列外另一種最廣泛應用的基礎資料結構的原因。

事實上,java中的LinkedList就實現了Deque介面。

  1. publicclassLinkedList<E>
  2. extendsAbstractSequentialList<E>
  3. implementsList<E>,Deque<E>,Cloneable,java.io.Serializable

因此LinkedList具有Deque和Queue的所有功能。