一、簡介
前文介紹了《最大堆》的實現,本章節在最大堆的基礎上實現一個簡單的優先隊列。優先隊列的實現本身沒什么難度,所以本文我們從優先隊列的場景出發介紹topK問題。
后面會持續更新數據結構相關的博文。
數據結構專欄:https://www.cnblogs.com/hello-shf/category/1519192.html
git傳送門:https://github.com/hello-shf/data-structure.git
二、優先隊列
普通的隊列是一種先進先出的數據結構,元素在隊列尾追加,而從隊列頭刪除。在優先隊列中,元素被賦予優先級。當訪問元素時,具有最高優先級的元素最先刪除。優先隊列具有最高級先出 (first in, largest out)的行為特征。通常采用堆數據結構來實現。
上面是百度百科給出的優先隊列的解釋。解釋的還是很到位的。具體的優先隊列的實現可以采用最小堆或者最大堆。因為在我們前文《最大堆》的實現中,該堆存儲的元素是要求實現Comparable接口的。所以優先級是掌握在用戶手中的,所以最小堆和最大堆都可以作為優先隊列的底層數據結構。
普通的隊列Queue,我們都知道是先進先出(FIFO)的,所以元素的出隊順序和入隊順序是保持一致的。但是對於我們的優先隊列,出隊操作,將不保證先進先出的隊列特性,而是根據元素的優先級(或者說權重)決定出隊的順序。
如果最小元素擁有最高的優先級,那么這種優先隊列叫作升序優先隊列,即總是優先刪除最小的元素。同理,如果最大元素擁有最高的優先級,那么這種優先隊列叫作降序優先隊列,即總是先刪除最大的元素。
優先隊列的使用場景:
算法場景:
最短路徑算法:Dijkstra算法
最小生成樹算法:Prim算法
事件驅動仿真:顧客排隊算法
選擇問題:查找第k個最小元素
實現場景:
游戲中優先攻擊最近單位,優先攻擊血量最低等
售票窗口的老幼病殘孕和軍人優先購票等
三、優先隊列的實現
3.1、隊列接口定義
同普通的隊列,我們先定義隊列的接口如下
1 /** 2 * 描述:隊列 3 * 4 * @Author shf 5 * @Date 2019/7/18 15:30 6 * @Version V1.0 7 **/ 8 public interface Queue<E> { 9 /** 10 * 獲取當前隊列的元素數 11 * @return 12 */ 13 int getSize(); 14 15 /** 16 * 判斷當前隊列是否為空 17 * @return 18 */ 19 boolean isEmpty(); 20 21 /** 22 * 入隊操作 23 * @param e 24 */ 25 void enqueue(E e); 26 27 /** 28 * 出隊操作 29 * @return 30 */ 31 E dequeue(); 32 33 /** 34 * 獲取隊列頭元素 35 * @return 36 */ 37 E getFront(); 38 }
3.2、最大堆實現的優先隊列
我們使用前文實現的《最大堆》來實現一個優先隊列
1 /** 2 * 描述:優先隊列 3 * 4 * @Author shf 5 * @Date 2019/7/18 17:31 6 * @Version V1.0 7 **/ 8 public class PriorityQueue<E extends Comparable<E>> implements Queue<E> { 9 10 private MaxHeap<E> maxHeap; 11 12 public PriorityQueue(){ 13 maxHeap = new MaxHeap<>(); 14 } 15 16 @Override 17 public int getSize(){ 18 return maxHeap.size(); 19 } 20 21 @Override 22 public boolean isEmpty(){ 23 return maxHeap.isEmpty(); 24 } 25 26 @Override 27 public E getFront(){ 28 // 獲取隊列的頭元素,在最大堆中就是獲取堆頂元素 29 return maxHeap.findMax(); 30 } 31 32 @Override 33 public void enqueue(E e){ 34 // 壓棧 直接向最大堆中添加,讓最大堆的add方法維護 元素的優先級 35 maxHeap.add(e); 36 } 37 38 @Override 39 public E dequeue(){ 40 // 出棧 將最大堆的堆頂元素取出 41 return maxHeap.extractMax(); 42 } 43 }
需要解釋的都在代碼注釋中了。
到這里優先隊列就實現完了,是不是很簡單。
在java中也有一個類PriorityQueue,其底層是采用的最小堆實現的優先隊列。在java PriorityQueue中關於優先級的定義,優先級隊列的元素按照其自然順序進行排序,或者根據構造隊列時提供的 Comparator 進行排序,具體取決於所使用的構造方法。底層數據結構最大堆或者最小堆是沒有什么區別的。關鍵在於我們如何定義優先級。
四、topK問題
關於topK問題,leetcode上面有一道典型的題目
題目最終需要返回的是前 k 個頻率最大的元素,可以想到借助堆這種數據結構,對於 k 頻率之后的元素不用再去處理,進一步優化時間復雜度。
具體操作為:
借助 哈希表 來建立數字和其出現次數的映射,遍歷一遍數組統計元素的頻率
維護一個元素數目為 k 的最小堆
每次都將新的元素與堆頂元素(堆中頻率最小的元素)進行比較
如果新的元素的頻率比堆頂端的元素大,則彈出堆頂端的元素,將新的元素添加進堆中
最終,堆中的 k 個元素即為前 k 個高頻元素
具體實現
1 class Solution { 2 public List<Integer> topKFrequent(int[] nums, int k) { 3 // 使用字典,統計每個元素出現的次數,元素為鍵,元素出現的次數為值 4 HashMap<Integer,Integer> map = new HashMap(); 5 for(int num : nums){ 6 if (map.containsKey(num)) { 7 map.put(num, map.get(num) + 1); 8 } else { 9 map.put(num, 1); 10 } 11 } 12 // 遍歷map,用最小堆保存頻率最大的k個元素 13 PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() { 14 @Override 15 public int compare(Integer a, Integer b) { 16 return map.get(a) - map.get(b); 17 } 18 }); 19 for (Integer key : map.keySet()) { 20 if (pq.size() < k) { 21 pq.add(key); 22 } else if (map.get(key) > map.get(pq.peek())) { 23 pq.remove(); 24 pq.add(key); 25 } 26 } 27 // 取出最小堆中的元素 28 List<Integer> res = new ArrayList<>(); 29 while (!pq.isEmpty()) { 30 res.add(pq.remove()); 31 } 32 return res; 33 } 34 }
以上是使用java原生的優先隊列實現的。接下來我們用我們自己實現的PriorityQueue試驗一下。
首先因為我們沒有提供接收一個Comparator的構造器,所以我們通過定義一個類來完成這個過程比較。
因為自己定義的優先隊列底層使用的是我們自己實現的最大堆,以及最大堆底層數組也是使用自己定義的,所以我們在leetcode提交驗證的時候,需要將這些自定義的類以內部類的方式提交上去。整體代碼如下

1 /// 347. Top K Frequent Elements 2 /// https://leetcode.com/problems/top-k-frequent-elements/description/ 3 4 import java.util.LinkedList; 5 import java.util.List; 6 import java.util.TreeMap; 7 8 class Solution { 9 10 private class Array<E> { 11 12 private E[] data; 13 private int size; 14 15 // 構造函數,傳入數組的容量capacity構造Array 16 public Array(int capacity){ 17 data = (E[])new Object[capacity]; 18 size = 0; 19 } 20 21 // 無參數的構造函數,默認數組的容量capacity=10 22 public Array(){ 23 this(10); 24 } 25 26 public Array(E[] arr){ 27 data = (E[])new Object[arr.length]; 28 for(int i = 0 ; i < arr.length ; i ++) 29 data[i] = arr[i]; 30 size = arr.length; 31 } 32 33 // 獲取數組的容量 34 public int getCapacity(){ 35 return data.length; 36 } 37 38 // 獲取數組中的元素個數 39 public int getSize(){ 40 return size; 41 } 42 43 // 返回數組是否為空 44 public boolean isEmpty(){ 45 return size == 0; 46 } 47 48 // 在index索引的位置插入一個新元素e 49 public void add(int index, E e){ 50 51 if(index < 0 || index > size) 52 throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size."); 53 54 if(size == data.length) 55 resize(2 * data.length); 56 57 for(int i = size - 1; i >= index ; i --) 58 data[i + 1] = data[i]; 59 60 data[index] = e; 61 62 size ++; 63 } 64 65 // 向所有元素后添加一個新元素 66 public void addLast(E e){ 67 add(size, e); 68 } 69 70 // 在所有元素前添加一個新元素 71 public void addFirst(E e){ 72 add(0, e); 73 } 74 75 // 獲取index索引位置的元素 76 public E get(int index){ 77 if(index < 0 || index >= size) 78 throw new IllegalArgumentException("Get failed. Index is illegal."); 79 return data[index]; 80 } 81 82 // 修改index索引位置的元素為e 83 public void set(int index, E e){ 84 if(index < 0 || index >= size) 85 throw new IllegalArgumentException("Set failed. Index is illegal."); 86 data[index] = e; 87 } 88 89 // 查找數組中是否有元素e 90 public boolean contains(E e){ 91 for(int i = 0 ; i < size ; i ++){ 92 if(data[i].equals(e)) 93 return true; 94 } 95 return false; 96 } 97 98 // 查找數組中元素e所在的索引,如果不存在元素e,則返回-1 99 public int find(E e){ 100 for(int i = 0 ; i < size ; i ++){ 101 if(data[i].equals(e)) 102 return i; 103 } 104 return -1; 105 } 106 107 // 從數組中刪除index位置的元素, 返回刪除的元素 108 public E remove(int index){ 109 if(index < 0 || index >= size) 110 throw new IllegalArgumentException("Remove failed. Index is illegal."); 111 112 E ret = data[index]; 113 for(int i = index + 1 ; i < size ; i ++) 114 data[i - 1] = data[i]; 115 size --; 116 data[size] = null; // loitering objects != memory leak 117 118 if(size == data.length / 4 && data.length / 2 != 0) 119 resize(data.length / 2); 120 return ret; 121 } 122 123 // 從數組中刪除第一個元素, 返回刪除的元素 124 public E removeFirst(){ 125 return remove(0); 126 } 127 128 // 從數組中刪除最后一個元素, 返回刪除的元素 129 public E removeLast(){ 130 return remove(size - 1); 131 } 132 133 // 從數組中刪除元素e 134 public void removeElement(E e){ 135 int index = find(e); 136 if(index != -1) 137 remove(index); 138 } 139 140 public void swap(int i, int j){ 141 142 if(i < 0 || i >= size || j < 0 || j >= size) 143 throw new IllegalArgumentException("Index is illegal."); 144 145 E t = data[i]; 146 data[i] = data[j]; 147 data[j] = t; 148 } 149 150 @Override 151 public String toString(){ 152 153 StringBuilder res = new StringBuilder(); 154 res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length)); 155 res.append('['); 156 for(int i = 0 ; i < size ; i ++){ 157 res.append(data[i]); 158 if(i != size - 1) 159 res.append(", "); 160 } 161 res.append(']'); 162 return res.toString(); 163 } 164 165 // 將數組空間的容量變成newCapacity大小 166 private void resize(int newCapacity){ 167 168 E[] newData = (E[])new Object[newCapacity]; 169 for(int i = 0 ; i < size ; i ++) 170 newData[i] = data[i]; 171 data = newData; 172 } 173 } 174 175 private class MaxHeap<E extends Comparable<E>> { 176 177 private Array<E> data; 178 179 public MaxHeap(int capacity){ 180 data = new Array<>(capacity); 181 } 182 183 public MaxHeap(){ 184 data = new Array<>(); 185 } 186 187 public MaxHeap(E[] arr){ 188 data = new Array<>(arr); 189 for(int i = parent(arr.length - 1) ; i >= 0 ; i --) 190 siftDown(i); 191 } 192 193 // 返回堆中的元素個數 194 public int size(){ 195 return data.getSize(); 196 } 197 198 // 返回一個布爾值, 表示堆中是否為空 199 public boolean isEmpty(){ 200 return data.isEmpty(); 201 } 202 203 // 返回完全二叉樹的數組表示中,一個索引所表示的元素的父親節點的索引 204 private int parent(int index){ 205 if(index == 0) 206 throw new IllegalArgumentException("index-0 doesn't have parent."); 207 return (index - 1) / 2; 208 } 209 210 // 返回完全二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引 211 private int leftChild(int index){ 212 return index * 2 + 1; 213 } 214 215 // 返回完全二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引 216 private int rightChild(int index){ 217 return index * 2 + 2; 218 } 219 220 // 向堆中添加元素 221 public void add(E e){ 222 data.addLast(e); 223 siftUp(data.getSize() - 1); 224 } 225 226 private void siftUp(int k){ 227 228 while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){ 229 data.swap(k, parent(k)); 230 k = parent(k); 231 } 232 } 233 234 // 看堆中的最大元素 235 public E findMax(){ 236 if(data.getSize() == 0) 237 throw new IllegalArgumentException("Can not findMax when heap is empty."); 238 return data.get(0); 239 } 240 241 // 取出堆中最大元素 242 public E extractMax(){ 243 244 E ret = findMax(); 245 246 data.swap(0, data.getSize() - 1); 247 data.removeLast(); 248 siftDown(0); 249 250 return ret; 251 } 252 253 private void siftDown(int k){ 254 255 while(leftChild(k) < data.getSize()){ 256 int j = leftChild(k); // 在此輪循環中,data[k]和data[j]交換位置 257 if( j + 1 < data.getSize() && 258 data.get(j + 1).compareTo(data.get(j)) > 0 ) 259 j ++; 260 // data[j] 是 leftChild 和 rightChild 中的最大值 261 262 if(data.get(k).compareTo(data.get(j)) >= 0 ) 263 break; 264 265 data.swap(k, j); 266 k = j; 267 } 268 } 269 270 // 取出堆中的最大元素,並且替換成元素e 271 public E replace(E e){ 272 273 E ret = findMax(); 274 data.set(0, e); 275 siftDown(0); 276 return ret; 277 } 278 } 279 280 private interface Queue<E> { 281 282 int getSize(); 283 boolean isEmpty(); 284 void enqueue(E e); 285 E dequeue(); 286 E getFront(); 287 } 288 289 private class PriorityQueue<E extends Comparable<E>> implements Queue<E> { 290 291 private MaxHeap<E> maxHeap; 292 293 public PriorityQueue(){ 294 maxHeap = new MaxHeap<>(); 295 } 296 297 @Override 298 public int getSize(){ 299 return maxHeap.size(); 300 } 301 302 @Override 303 public boolean isEmpty(){ 304 return maxHeap.isEmpty(); 305 } 306 307 @Override 308 public E getFront(){ 309 return maxHeap.findMax(); 310 } 311 312 @Override 313 public void enqueue(E e){ 314 maxHeap.add(e); 315 } 316 317 @Override 318 public E dequeue(){ 319 return maxHeap.extractMax(); 320 } 321 } 322 323 private class Freq implements Comparable<Freq>{ 324 325 public int e, freq; 326 327 public Freq(int e, int freq){ 328 this.e = e; 329 this.freq = freq; 330 } 331 332 @Override 333 public int compareTo(Freq another){ 334 if(this.freq < another.freq) 335 return 1; 336 else if(this.freq > another.freq) 337 return -1; 338 else 339 return 0; 340 } 341 } 342 343 public List<Integer> topKFrequent(int[] nums, int k) { 344 345 TreeMap<Integer, Integer> map = new TreeMap<>(); 346 for(int num: nums){ 347 if(map.containsKey(num)) 348 map.put(num, map.get(num) + 1); 349 else 350 map.put(num, 1); 351 } 352 353 PriorityQueue<Freq> pq = new PriorityQueue<>(); 354 for(int key: map.keySet()){ 355 if(pq.getSize() < k) 356 pq.enqueue(new Freq(key, map.get(key))); 357 else if(map.get(key) > pq.getFront().freq){ 358 pq.dequeue(); 359 pq.enqueue(new Freq(key, map.get(key))); 360 } 361 } 362 363 LinkedList<Integer> res = new LinkedList<>(); 364 while(!pq.isEmpty()) 365 res.add(pq.dequeue().e); 366 return res; 367 } 368 369 private static void printList(List<Integer> nums){ 370 for(Integer num: nums) 371 System.out.print(num + " "); 372 System.out.println(); 373 } 374 375 public static void main(String[] args) { 376 377 int[] nums = {1, 1, 1, 2, 2, 3}; 378 int k = 2; 379 printList((new Solution()).topKFrequent(nums, k)); 380 } 381 }
在以上代碼中我們需要關心的是如下部分
1 private class Freq implements Comparable<Freq>{ 2 3 public int e, freq; 4 5 public Freq(int e, int freq){ 6 this.e = e; 7 this.freq = freq; 8 } 9 10 @Override 11 public int compareTo(Freq another){ 12 if(this.freq < another.freq) 13 return 1; 14 else if(this.freq > another.freq) 15 return -1; 16 else 17 return 0; 18 } 19 } 20 21 public List<Integer> topKFrequent(int[] nums, int k) { 22 23 TreeMap<Integer, Integer> map = new TreeMap<>(); 24 for(int num: nums){ 25 if(map.containsKey(num)) 26 map.put(num, map.get(num) + 1); 27 else 28 map.put(num, 1); 29 } 30 31 PriorityQueue<Freq> pq = new PriorityQueue<>(); 32 for(int key: map.keySet()){ 33 if(pq.getSize() < k) 34 pq.enqueue(new Freq(key, map.get(key))); 35 else if(map.get(key) > pq.getFront().freq){ 36 pq.dequeue(); 37 pq.enqueue(new Freq(key, map.get(key))); 38 } 39 } 40 41 LinkedList<Integer> res = new LinkedList<>(); 42 while(!pq.isEmpty()) 43 res.add(pq.dequeue().e); 44 return res; 45 }
我們將完整代碼提交到leetcode
得到如下結果,表示我們驗證自己實現的優先隊列成功了。
這盛世,如您所願。
如有錯誤的地方還請留言指正。
原創不易,轉載請注明原文地址:https://www.cnblogs.com/hello-shf/p/11397386.html