日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

数据结构与算法(4)——优先队列和堆

發布時間:2025/3/21 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 数据结构与算法(4)——优先队列和堆 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言:題圖無關,接下來開始簡單學習學習優先隊列和堆的相關數據結構的知識;

前序文章:

  • 數據結構與算法(1)——數組與鏈表(https://www.jianshu.com/p/7b93b3570875)
  • 數據結構與算法(2)——棧和隊列(https://www.jianshu.com/p/5087c751cb42)
  • 數據結構與算法(3)——樹(二叉、二叉搜索樹)(https://www.jianshu.com/p/4ef1f50d45b5)

什么是優先隊列?

聽這個名字就能知道,優先隊列也是一種隊列,只不過不同的是,優先隊列的出隊順序是按照優先級來的;在有些情況下,可能需要找到元素集合中的最小或者最大元素,可以利用優先隊列ADT來完成操作,優先隊列ADT是一種數據結構,它支持插入和刪除最小值操作(返回并刪除最小元素)或刪除最大值操作(返回并刪除最大元素);

這些操作等價于隊列的enQueue和deQueue操作,區別在于,對于優先隊列,元素進入隊列的順序可能與其被操作的順序不同,作業調度是優先隊列的一個應用實例,它根據優先級的高低而不是先到先服務的方式來進行調度;

如果最小鍵值元素擁有最高的優先級,那么這種優先隊列叫作升序優先隊列(即總是先刪除最小的元素),類似的,如果最大鍵值元素擁有最高的優先級,那么這種優先隊列叫作降序優先隊列(即總是先刪除最大的元素);由于這兩種類型時對稱的,所以只需要關注其中一種,如升序優先隊列;

優先隊列ADT

下面操作組成了優先隊列的一個ADT;

1.優先隊列的主要操作
優先隊列是元素的容器,每個元素有一個相關的鍵值;

  • insert(key, data):插入鍵值為key的數據到優先隊列中,元素以其key進行排序;
  • deleteMin/deleteMax:刪除并返回最小/最大鍵值的元素;
  • getMinimum/getMaximum:返回最小/最大劍指的元素,但不刪除它;

2.優先隊列的輔助操作

  • 第k最小/第k最大:返回優先隊列中鍵值為第k個最小/最大的元素;
  • 大小(size):返回優先隊列中的元素個數;
  • 堆排序(Heap Sort):基于鍵值的優先級將優先隊列中的元素進行排序;

優先隊列的應用

  • 數據壓縮:赫夫曼編碼算法;
  • 最短路徑算法:Dijkstra算法;
  • 最小生成樹算法:Prim算法;
  • 事件驅動仿真:顧客排隊算法;
  • 選擇問題:查找第k個最小元素;
  • 等等等等....

優先隊列的實現比較

實現插入刪除尋找最小值
無序數組1nn
無序鏈表1nn
有序數組n11
有序鏈表n11
二叉搜索樹logn(平均)logn(平均)logn(平均)
平衡二叉搜索樹lognlognlogn
二叉堆lognlogn1

堆和二叉堆

什么是堆

堆是一顆具有特定性質的二叉樹,堆的基本要求就是堆中所有結點的值必須大于或等于(或小于或等于)其孩子結點的值,這也稱為堆的性質;堆還有另一個性質,就是當 h > 0 時,所有葉子結點都處于第 h 或 h - 1 層(其中 h 為樹的高度,完全二叉樹),也就是說,堆應該是一顆完全二叉樹;

在下面的例子中,左邊的樹為堆(每個元素都大于其孩子結點的值),而右邊的樹不是堆(因為5大于其孩子結點2)

二叉堆

在二叉堆中,每個結點最多有兩個孩子結點,在實際應用中,二叉堆已經足夠滿足需求,因此接下來主要討論二叉最小堆和二叉最大堆;

堆的表示:在描述堆的操作前,首先來看堆是怎樣表示的,一種可能的方法就是使用數組,因為堆在形式上是一顆完全二叉樹,用數組來存儲它不會浪費任何空間,例如下圖:

用數組來表示堆不僅不會浪費空間還具有一定的優勢:

  • 每個結點的左孩子為下標i的2倍:left child(i) = i * 2;每個結點的右孩子為下標i的2倍加1:right child(i) = i * 2 + 1
  • 每個結點的父親結點為下標的二分之一:parent(i) = i / 2,注意這里是整數除,2和3除以2都為1,大家可以驗證一下;
  • 注意:這里是把下標為0的地方空出來了的,主要是為了方便理解,如果0不空出來只需要在計算的時候把i值往右偏移一個位置就行了(也就是加1,大家可以試試,下面的演示也采取這樣的方式);

二叉堆的相關操作

堆的基本結構

public class MaxHeap<E extends Comparable<E>> {private Array<E> data;public MaxHeap(int capacity){ data = new Array<>(capacity); }public MaxHeap(){ data = new Array<>(); }// 返回堆中的元素個數public int size(){ return data.getSize(); }// 返回一個布爾值, 表示堆中是否為空public boolean isEmpty(){ return data.isEmpty(); }// 返回完全二叉樹的數組表示中,一個索引所表示的元素的父親節點的索引private int parent(int index){if(index == 0)throw new IllegalArgumentException("index-0 doesn't have parent.");return (index - 1) / 2;}// 返回完全二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引private int leftChild(int index){ return index * 2 + 1; }// 返回完全二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引private int rightChild(int index){ return index * 2 + 2; } }

向堆中添加元素和Sift Up

當插入一個元素到堆中時,它可能不滿足堆的性質,在這種情況下,需要調整堆中元素的位置使之重新變成堆,這個過程稱為堆化(heapifying);在最大堆中,要堆化一個元素,需要找到它的父親結點,如果不滿足堆的基本性質則交換兩個元素的位置,重復該過程直到每個結點都滿足堆的性質為止,下面我們來模擬一下這個過程:

下面我們在該堆中插入一個新的元素26:

我們通過索引(上面的公式)可以很容易地找到新插入元素的父親結點,然后比較它們的大小,如果新元素更大則交換兩個元素的位置,這個操作就相當于把該元素上浮了一下:

重復該操作直到26到了一個滿足堆條件的位置,此時就完成了插入的操作:

對應的代碼如下:

// 向堆中添加元素 public void add(E e){data.addLast(e);siftUp(data.getSize() - 1); }private void siftUp(int k){while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){data.swap(k, parent(k));k = parent(k);} }

取出堆中的最大元素和Sift Down

如果理解了上述的過程,那么取出堆中的最大元素(堆頂元素)將變得容易,不過這里運用到一個小技巧,就是用最后一個元素替換掉棧頂元素,然后把最后一個元素刪除掉,這樣一來元素的總個數也滿足條件,然后只需要把棧頂元素依次往下調整就好了,這個操作就叫做Sift Down(下沉):

用最后元素替換掉棧頂元素,然后刪除最后一個元素:

然后比較其孩子結點的大小:

如果不滿足堆的條件,那么就跟孩子結點中較大的一個交換位置:

重復該步驟,直到16到達合適的位置:

完成取出最大元素的操作:

對應的代碼如下:

// 看堆中的最大元素 public E findMax(){if(data.getSize() == 0)throw new IllegalArgumentException("Can not findMax when heap is empty.");return data.get(0); }// 取出堆中最大元素 public E extractMax(){E ret = findMax();data.swap(0, data.getSize() - 1);data.removeLast();siftDown(0);return ret; }private void siftDown(int k){while(leftChild(k) < data.getSize()){int j = leftChild(k); // 在此輪循環中,data[k]和data[j]交換位置if( j + 1 < data.getSize() &&data.get(j + 1).compareTo(data.get(j)) > 0 )j ++;// data[j] 是 leftChild 和 rightChild 中的最大值if(data.get(k).compareTo(data.get(j)) >= 0 )break;data.swap(k, j);k = j;} }

Replace 和 Heapify

Replace這個操作其實就是取出堆中最大的元素之后再新插入一個元素,常規的做法是取出最大元素之后,再利用上面的插入新元素的操作對堆進行Sift Up操作,但是這里有一個小技巧就是直接使用新元素替換掉堆頂元素,之后再進行Sift Down操作,這樣就把兩次O(logn)的操作變成了一次O(logn):

// 取出堆中的最大元素,并且替換成元素e public E replace(E e){E ret = findMax();data.set(0, e);siftDown(0);return ret; }

Heapify翻譯過來就是堆化的意思,就是將任意數組整理成堆的形狀,通常的做法是遍歷數組從0開始添加創建一個新的堆,但是這里存在一個小技巧就是把當前數組就看做是一個完全二叉樹,然后從最后一個非葉子結點開始進行Sift Down操作就可以了,最后一個非葉子結點也很好找,就是最后一個結點的父親結點,大家可以驗證一下:

從22這個節點開始,依次開始Sift Down操作:

重復該過程直到堆頂元素:

完成堆化操作:

將n個元素逐個插入到一個空堆中,算法復雜度是O(nlogn),而heapify的過程,算法復雜度為O(n),這是有一個質的飛躍的,下面是代碼:

public MaxHeap(E[] arr){data = new Array<>(arr);for(int i = parent(arr.length - 1) ; i >= 0 ; i --)siftDown(i); }

基于堆的優先隊列

首先我們的隊列仍然需要繼承我們之前將隊列時候聲明的哪個接口Queue,然后實現這個接口中的方法就可以了,之類簡單寫一下:

public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {private MaxHeap<E> maxHeap;public PriorityQueue(){ maxHeap = new MaxHeap<>(); }@Overridepublic int getSize(){ return maxHeap.size(); }@Overridepublic boolean isEmpty(){ return maxHeap.isEmpty(); }@Overridepublic E getFront(){ return maxHeap.findMax(); }@Overridepublic void enqueue(E e){ maxHeap.add(e); }@Overridepublic E dequeue(){ return maxHeap.extractMax(); } }

Java中的PriorityQueue

在Java中也實現了自己的優先隊列java.util.PriorityQueue,與我們自己寫的不同之處在于,Java中內置的為最小堆,然后就是一些函數名不一樣,底層還是維護了一個Object類型的數組,大家可以戳戳看有什么不同,另外如果想要把最小堆變成最大堆可以給PriorityQueue傳入自己的比較器,例如:

// 默認為最小堆 PriorityQueue<Integer> pq = new PriorityQueue<>();pq.add(5); pq.add(2); pq.add(1); pq.add(10); pq.add(3);while (!pq.isEmpty()) {System.out.println(pq.poll() + ", "); } System.out.println(); System.out.println("————————————————————————");// 使用Lambda表達式傳入自己的比較器轉換成最大堆 PriorityQueue<Integer> pq2 = new PriorityQueue<>((a, b) -> b - a); pq2.add(5); pq2.add(2); pq2.add(1); pq2.add(10); pq2.add(3);while (!pq2.isEmpty()) {System.out.println(pq2.poll() + ", "); }

LeetCode相關題目整理

23. 合并K個排序鏈表

參考答案:(85ms)

public ListNode mergeKLists(ListNode[] lists) {if (lists == null || lists.length == 0) return null;PriorityQueue<ListNode> q = new PriorityQueue<>(Comparator.comparing(node -> node.val));for (int i = 0; i < lists.length; i++) {if (lists[i] != null) {q.add(lists[i]);}}ListNode dummy = new ListNode(0);ListNode tail = dummy;while (!q.isEmpty()) {tail.next = q.poll();tail = tail.next;if (tail.next != null) {q.add(tail.next);}}return dummy.next; }

215. 數組中的第K個最大元素

我的答案:(75ms)

public int findKthLargest(int[] nums, int k) {// 正確性判斷if (0 == nums.length || null == nums || k <= 0 || k > nums.length) {return -1;}// 構造優先隊列,默認為最小堆,傳入自定義的比較器轉換成最大堆PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a);for (Integer num : nums) {pq.add(num);}for (int i = 0; i < k - 1; i++) {pq.remove();}return pq.peek(); }

參考答案:(5ms)

public int findKthLargest(int[] nums, int k) {if (nums.length == 1) {return nums[0];}int max = nums[0];int min = nums[0];for (int i : nums) {max = i > max ? i : max;min = i < min ? i : min;}int[] arrs = new int[max - min + 1];for (int i : nums) {arrs[max - i]++;}int pos = 0;for (int i = 0; i < arrs.length; i++) {pos += arrs[i];if (pos >= k) {return max - i;}}return nums[0]; }

還看到一個簡單粗暴的,也是服了:(4ms)

public int findKthLargest(int[] nums, int k) {Arrays.sort(nums);return nums[nums.length - k]; }

而且我隨機生成了一個100萬數據的隨機數組,來測試這個簡單粗暴的方法的效率,發現當數據量上去之后,排序這個操作變得繁瑣,我自己測試的時候,上面三個方法,第三個大概比第一個(我自己寫的方法)多花僅4倍的時間;

239. 滑動窗口最大值(類似劍指Offer面試題59)

參考答案:(88ms)

public int[] maxSlidingWindow(int[] nums, int k) {if (nums == null || k <= 0) return new int[0];int[] res = new int[nums.length - k + 1];ArrayDeque<Integer> deque = new ArrayDeque<Integer>();int index = 0;for (int i = 0; i < nums.length; i++) {while (!deque.isEmpty() && deque.peek() < i - k + 1) // Ensure deque's size doesn't exceed kdeque.poll();// Remove numbers smaller than a[i] from right(a[i-1]) to left, to make the first number in the deque the largest one in the window while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i])deque.pollLast();deque.offer(i);// Offer the current index to the deque's tailif (i >= k - 1)// Starts recording when i is big enough to make the window has k elements res[index++] = nums[deque.peek()];}return res; }

參考答案2:(9ms)

public int[] maxSlidingWindow(int[] nums, int k) { /* 思想:依次遍歷數組,有效范圍在長度k內尋找當前最大值,在用result數組來依次存儲當前長度K內的最大值;若在當前輪中出現新增的nums[end]大于curMax,直接替換即可;如果當前輪curMax不是新增的nums[end],在新的范圍內重置curMax. */if (nums.length == 0 || k <= 0)return new int[0];int curMax = Integer.MIN_VALUE;for (int i = 0; i < k; i++) {if (nums[i] > curMax)curMax = nums[i];}int[] ans = new int[nums.length - k + 1];ans[0] = curMax;for (int start = 1; start + k - 1 < nums.length; start++) {int end = start + k - 1;if (nums[end] > curMax)curMax = nums[end];else if (nums[start - 1] == curMax) {//新增的不大于curMax,新范圍內重置curMax = Integer.MIN_VALUE;for (int i = start; i <= end; i++) {if (nums[i] > curMax)curMax = nums[i];}}ans[start] = curMax;}return ans; }

264. 丑數 II(劍指Offer面試題49)

參考答案:(7ms)

public int nthUglyNumber(int n) {// 正確性判斷if (n < 1 || n > 1690) {return -1;}int[] ugly = new int[n];ugly[0] = 1;int index2 = 0, index3 = 0, index5 = 0;int factor2 = 2, factor3 = 3, factor5 = 5;for (int i = 1; i < n; i++) {int min = Math.min(Math.min(factor2, factor3), factor5);ugly[i] = min;if (factor2 == min)factor2 = 2 * ugly[++index2];if (factor3 == min)factor3 = 3 * ugly[++index3];if (factor5 == min)factor5 = 5 * ugly[++index5];}return ugly[n - 1]; }

如果采用逐個判斷每個整數是不是丑數的解法,直觀但不夠高效,所以我們就需要換一種思路,我的第一反應就是這其中一定有什么規律,但是嘗試著找了一下,沒找到...看了看答案才幡然醒悟,前面提到的算法之所以效率低,很大程度上是因為不管一個數是不是丑數,我們都要對它進行計算,接下來我們試著找到一種只計算丑數的方法,而不在非丑數的整數上花費時間,根據丑數的定義,丑數應該是另一個丑數乘以2、3或者5的結果(1除外),因此,我們可以創建一個數組,里面的數字是排好序的丑數,每個丑數都是前面的丑數乘以2、3或者5得到的,也就是上面的算法了..

295.數據流的中位數(劍指Offer面試題41)

參考答案:(219ms)

public class MedianFinder {PriorityQueue<Integer> maxHeap;PriorityQueue<Integer> minHeap;/*** initialize your data structure here.*/public MedianFinder() {maxHeap = new PriorityQueue<>(Collections.reverseOrder());minHeap = new PriorityQueue<>();}public void addNum(int num) {maxHeap.add(num);minHeap.add(maxHeap.poll());if (minHeap.size() - maxHeap.size() > 0) {maxHeap.add(minHeap.poll());}}public double findMedian() {if (maxHeap.size() == minHeap.size()) {return (maxHeap.peek() + minHeap.peek()) / 2.0;} else {return maxHeap.peek();}} }

思路:這道題的實現思路有很多,比如我們可以在插入的時候就將每個元素插入到正確的位置上,這樣返回中位數的時候就會是一個O(1)的操作,下面列舉一張表來說明不同實現的復雜度具體是多少:

數據結構插入的時間復雜度得到中位數的時間復雜度
沒有排序的數組O(1)O(n)
排序的數組O(n)O(1)
排序的鏈表O(n)O(1)
二叉搜索樹平均O(logn),最差O(n)平均O(logn),最差O(n)
AVL樹O(logn)O(logn)
最大堆和最小堆O(logn)O(logn)

AVL樹是一種很高效的數據結構,但是在大多數的語言中都沒有現成的實現,所以考慮用最大堆和最小堆,對于一個已經排好序的數據容器,我們可以從中間分開分成兩個部分,其中拿P1指向左半部分最大的元素,拿P2指向有半部分最小的元素,如果能夠保證數據容器左邊的數據都小于右邊的數據,那么即使左、右兩邊內部的數據沒有排序,我們仍然可以根據左邊最大的數和右邊最大的數得到中位數:

如何快速從一個數據容器中找出最大數呢?我們可以使用最大堆來實現這個數據容器,因為堆頂的元素就是最大的元素;同樣我們可以使用最小堆來快速找出一個數據容器中最小數。因此按照這個思路我們就可以使用一個最大堆實現左邊的數據容器,使用一個最小堆實現右邊的數據容器,但是需要注意的是這兩個容器的大小差值不能超過1;

347. 前K個高頻元素(類似劍指Offer面試題40)

參考答案:(131ms)

public List<Integer> topKFrequent(int[] nums, int k) {TreeMap<Integer, Integer> map = new TreeMap<>();// 保存頻率for (int num : nums) {if (map.containsKey(num)) {map.put(num, map.get(num) + 1);} else {map.put(num, 1);}}PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(map::get));for (int key : map.keySet()) {if (pq.size() < k) {pq.add(key);} else if (map.get(key) > map.get(pq.peek())) {pq.remove();pq.add(key);}}LinkedList<Integer> res = new LinkedList<>();while (!pq.isEmpty()) {res.add(pq.remove());}return res; }

692. 前K個高頻單詞

參考答案:(72ms)

public List<String> topKFrequent(String[] words, int k) {Map<String, Integer> count = new HashMap();for (String word: words) {count.put(word, count.getOrDefault(word, 0) + 1);}List<String> candidates = new ArrayList(count.keySet());Collections.sort(candidates, (w1, w2) -> count.get(w1).equals(count.get(w2)) ?w1.compareTo(w2) : count.get(w2) - count.get(w1));return candidates.subList(0, k); }

這道題類似于上面的第347題,但是問題出在返回的順序上,需要自己來定義一個比較器來排序..然后也學到一個寫法,就是上面的第一個for循環里,getOrDefault()方法,get√..

參考答案2:(91ms)

public List<String> topKFrequent(String[] words, int k) {Map<String, Integer> count = new HashMap();for (String word: words) {count.put(word, count.getOrDefault(word, 0) + 1);}PriorityQueue<String> heap = new PriorityQueue<String>((w1, w2) -> count.get(w1).equals(count.get(w2)) ?w2.compareTo(w1) : count.get(w1) - count.get(w2) );for (String word: count.keySet()) {heap.offer(word);if (heap.size() > k) heap.poll();}List<String> ans = new ArrayList();while (!heap.isEmpty()) ans.add(heap.poll());Collections.reverse(ans);return ans; }

這個解法就有點兒類似于上面的347題,其實是大同小異,就是自己不會靈活使用比較器而已,學習到了學習到了√...


簡單總結

今天算是很有收獲的一天,因為這兩種數據結構都是自己特別不熟悉的,特別是在刷了一些LeetCode相關題目之后,對這兩種數據有了很不一樣的認識,特別是堆的應用,這是一種特別適合用來找第k小/大的特殊的數據結構,并且在Java中居然有直接的實現,這可太棒了,而且今天的效率還算挺高的,滿足;

歡迎轉載,轉載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz

總結

以上是生活随笔為你收集整理的数据结构与算法(4)——优先队列和堆的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。