隨筆-13  評論-22  文章-0  trackbacks-0


          PriorityQueue

          本文github地址

          總體介紹

          前面以Java ArrayDeque為例講解了StackQueue,其實還有一種特殊的隊列叫做PriorityQueue,即優先隊列。優先隊列的作用是能保證每次取出的元素都是隊列中權值最小的(Java的優先隊列每次取最小元素,C++的優先隊列每次取最大元素)。這里牽涉到了大小關系,元素大小的評判可以通過元素本身的自然順序(natural ordering),也可以通過構造時傳入的比較器Comparator,類似于C++的仿函數)。

          Java中PriorityQueue實現了Queue接口,不允許放入null元素;其通過堆實現,具體說是通過完全二叉樹(complete binary tree)實現的小頂堆(任意一個非葉子節點的權值,都不大于其左右子節點的權值),也就意味著可以通過數組來作為PriorityQueue的底層實現。

          PriorityQueue_base.png

          上圖中我們給每個元素按照層序遍歷的方式進行了編號,如果你足夠細心,會發現父節點和子節點的編號是有聯系的,更確切的說父子節點的編號之間有如下關系:

          leftNo = parentNo*2+1

          rightNo = parentNo*2+2

          parentNo = (nodeNo-1)/2

          通過上述三個公式,可以輕易計算出某個節點的父節點以及子節點的下標。這也就是為什么可以直接用數組來存儲堆的原因。

          PriorityQueuepeek()element操作是常數時間,add(), offer(), 無參數的remove()以及poll()方法的時間復雜度都是log(N)

          方法剖析

          add()和offer()

          add(E e)offer(E e)的語義相同,都是向優先隊列中插入元素,只是Queue接口規定二者對插入失敗時的處理不同,前者在插入失敗時拋出異常,后則則會返回false。對于PriorityQueue這兩個方法其實沒什么差別。

          PriorityQueue_offer.png

          新加入的元素可能會破壞小頂堆的性質,因此需要進行必要的調整。

          //offer(E e)
          public boolean offer(E e) {
              if (e == null)//不允許放入null元素
                  throw new NullPointerException();
              modCount++;
              int i = size;
              if (i >= queue.length)
                  grow(i + 1);//自動擴容
              size = i + 1;
              if (i == 0)//隊列原來為空,這是插入的第一個元素
                  queue[0] = e;
              else
                  siftUp(i, e);//調整
              return true;
          }
          上述代碼中,擴容函數grow()類似于ArrayList里的grow()函數,就是再申請一個更大的數組,并將原數組的元素復制過去,這里不再贅述。需要注意的是siftUp(int k, E x)方法,該方法用于插入元素x并維持堆的特性。
          //siftUp()
          private void siftUp(int k, E x) {
              while (k > 0) {
                  int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2
                  Object e = queue[parent];
                  if (comparator.compare(x, (E) e) >= 0)//調用比較器的比較方法
                      break;
                  queue[k] = e;
                  k = parent;
              }
              queue[k] = x;
          }
          新加入的元素x可能會破壞小頂堆的性質,因此需要進行調整。調整的過程為:k指定的位置開始,將x逐層與當前點的parent進行比較并交換,直到滿足x >= queue[parent]為止。注意這里的比較可以是元素的自然順序,也可以是依靠比較器的順序。

          element()和peek()

          element()peek()的語義完全相同,都是獲取但不刪除隊首元素,也就是隊列中權值最小的那個元素,二者唯一的區別是當方法失敗時前者拋出異常,后者返回null。根據小頂堆的性質,堆頂那個元素就是全局最小的那個;由于堆用數組表示,根據下標關系,0下標處的那個元素既是堆頂元素。所以直接返回數組0下標處的那個元素即可。

          PriorityQueue_peek.png

          代碼也就非常簡潔:

          //peek()
          public E peek() {
              if (size == 0)
                  return null;
              return (E) queue[0];//0下標處的那個元素就是最小的那個
          }

          remove()和poll()

          remove()poll()方法的語義也完全相同,都是獲取并刪除隊首元素,區別是當方法失敗時前者拋出異常,后者返回null。由于刪除操作會改變隊列的結構,為維護小頂堆的性質,需要進行必要的調整。

          PriorityQueue_poll.png
          代碼如下:

          public E poll() {
              if (size == 0)
                  return null;
              int s = --size;
              modCount++;
              E result = (E) queue[0];//0下標處的那個元素就是最小的那個
              E x = (E) queue[s];
              queue[s] = null;
              if (s != 0)
                  siftDown(0, x);//調整
              return result;
          }
          上述代碼首先記錄0下標處的元素,并用最后一個元素替換0下標位置的元素,之后調用siftDown()方法對堆進行調整,最后返回原來0下標處的那個元素(也就是最小的那個元素)。重點是siftDown(int k, E x)方法,該方法的作用是k指定的位置開始,將x逐層向下與當前點的左右孩子中較小的那個交換,直到x小于或等于左右孩子中的任何一個為止。
          //siftDown()
          private void siftDown(int k, E x) {
              int half = size >>> 1;
              while (k < half) {
                  //首先找到左右孩子中較小的那個,記錄到c里,并用child記錄其下標
                  int child = (k << 1) + 1;//leftNo = parentNo*2+1
                  Object c = queue[child];
                  int right = child + 1;
                  if (right < size &&
                      comparator.compare((E) c, (E) queue[right]) > 0)
                      c = queue[child = right];
                  if (comparator.compare(x, (E) c) <= 0)
                      break;
                  queue[k] = c;//然后用c取代原來的值
                  k = child;
              }
              queue[k] = x;
          }

          remove(Object o)

          remove(Object o)方法用于刪除隊列中跟o相等的某一個元素(如果有多個相等,只刪除一個),該方法不是Queue接口內的方法,而是Collection接口的方法。由于刪除操作會改變隊列結構,所以要進行調整;又由于刪除元素的位置可能是任意的,所以調整過程比其它函數稍加繁瑣。具體來說,remove(Object o)可以分為2種情況:1. 刪除的是最后一個元素。直接刪除即可,不需要調整。2. 刪除的不是最后一個元素。此處不再贅述。

          PriorityQueue_remove2.png

          具體代碼如下:

          //remove(Object o)
          public boolean remove(Object o) {
              //通過遍歷數組的方式找到第一個滿足o.equals(queue[i])元素的下標
              int i = indexOf(o);
              if (i == -1)
                  return false;
              int s = --size;
              if (s == i) //情況1
                  queue[i] = null;
              else {
                  E moved = (E) queue[s];
                  queue[s] = null;
                  siftDown(i, moved);//情況2
                  
              }
              return true;
          }

          posted on 2016-05-12 21:22 CarpenterLee 閱讀(1452) 評論(2)  編輯  收藏

          評論:
          # re: Java PriorityQueue源碼剖析 2016-05-15 18:36 | 有機綠茶
          剖析得很詳細,尤其是那些圖片,很形象,學習啦!  回復  更多評論
            
          # re: Java PriorityQueue源碼剖析 2016-05-16 07:27 | CarpenterLee
          @有機綠茶
          只有代碼太枯燥了,附圖更生動些。  回復  更多評論
            

          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 神农架林区| 凤山市| 广州市| 江山市| 上栗县| 河西区| 岳池县| 昔阳县| 贺兰县| 汉寿县| 柞水县| 寿阳县| 长治市| 克山县| 铁力市| 桦川县| 陆丰市| 玉溪市| 凯里市| 高要市| 张家港市| 海丰县| 东阳市| 东丽区| 铜川市| 甘谷县| 元谋县| 尉氏县| 赤峰市| 包头市| 廉江市| 左云县| 新宁县| 凤山市| 兰溪市| 四会市| 黄平县| 安西县| 芒康县| 岑巩县| 伽师县|