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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

算法原理系列:优先队列

發布時間:2023/12/20 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 算法原理系列:优先队列 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

 算法原理系列:優先隊列

第一次總結這種動態的數據結構,一如既往,看了大量的教程,上網搜優先隊列原理,能出來一大堆,但不知道為什么怎么這么多人搞不清楚原理和實現的區別?非要把實現講成原理,今天就說說自己對優先隊列的看法吧。

緣由

顧名思義,優先隊列是對隊列的一種改進,隊列為先進先出的一種數據結構,而優先隊列則保持一條性質:

在隊頭的原始始終保持優先級最高。

優先級最高,我們可以用一個數來衡量,所以簡單的想法就是時刻把持隊首的key元素最小(最小堆),或者key元素最大(最大堆)。

好了,開始構建優先隊列的結構吧,現假定給你一堆數:

nums = [78, 82, 75, 35, 71, 23, 41, 42, 58, 8]

目標:
每次輸出最小值,并從nums中刪除該元素,直到nums的大小為0。

很簡單,最navie的做法,每次遍歷一遍數組,找出最小值,輸出,并且置為INF,防止下次繼續遍歷,代碼如下:

public static void main(String[] args) {int[] test = RandomUtil.randomSet(1, 100, 10);solve(test);}static final int INF = 1 << 30;private static void solve(int[] nums){for (int i = 0; i < nums.length; i++){int index = -1, min = INF;for (int j = 0; j < nums.length; j++){if (nums[j] != INF && nums[j] < min){index = j;min = nums[j];}}System.out.println(min);nums[index] = INF;}}

最簡單的做法,但時間復雜度爆表,依次輸出n個元素時,時間復雜度為O(n2),找最小值操作也需要O(n),此處有幾個非常別捏的地方,該算法無法解決數組的動態擴展,而且把使用過的元素變為INF,所以我們需要設計一種動態擴展的數據結構來存儲不斷變動的nums,于是有了優先隊列的數據結構。

優先隊列API如下:

public class PriorityQueue<Key extends Comparable<Key>> {Key[] array;int N = 0; //記錄插入元素個數int SIZE; //當N>SIZE時,可以動態擴展@SuppressWarnings("unchecked")public PriorityQueue(int SIZE){this.SIZE = SIZE;array = (Key[]) new Comparable[SIZE];} public void offer(Key key){}public Key poll(){return null;}public Key peek(){return null;}public boolean isEmpty(){return false;} }

有了這樣的API,我們就可以把nums中的元素全部offer進去,而當想要輸出最小值時,直接poll出來即可,非常方便,想用就用,所以改進的做法如下:

public static void main(String[] args) {int[] test = RandomUtil.randomSet(1, 100, 10);PriorityQueue<Integer> queue = new PriorityQueue<>(10);for (int i = 0; i < test.length; i++){queue.offer(test[i]);}while (!queue.isEmpty())System.out.println(queue.poll());}

同樣能夠最大不斷輸出最小值,且支持動態加入元素,是不是高級很多。所以該問題就轉變成了設計優先隊列的API了。

API設計

開始吧,在《算法》書中介紹了初級優先隊列的實現,一種是基于stack操作的無序惰性算法,核心思想是,把所有的元素壓入棧中,不管它們的順序,只有當我需要最小值時,用O(n)的算法實現求最小,輸出。惰性源于盡量滿足插入常數級,不到迫不得已不去使用O(n)的操作。

另外一種IDEA是在插入時就保持元素的有序,這樣在取的操作,我們可以簡單的移動一個指針來不斷輸出最小值,所以為了【維持插入的有序】操作,有了時間復雜度為O(n)的插入算法,而在取最小時,可以O(1)

這是書中提到的兩個初級實現,可以說它們沒有什么特別的地方,想到也是理所當然的事,接下來就是實現細節的事了,我直接給出數組有序的版本(未實現動態擴展,感興趣的可以自己實現下)。

代碼如下:

/*** * @author DemonSong* * 實現基于插入排序的優先隊列* * 插入元素 O(n)* 刪除元素 O(n)** @param <T>*/ public class PriorityQueue<T extends Comparable<T>> {T[] array;int N = 0;int SIZE;@SuppressWarnings("unchecked")public PriorityQueue(int SIZE){this.SIZE = SIZE;array = (T[]) new Comparable[SIZE];}public void offer(T key){if (N == 0){array[N++] = key;return;}int i = 0;while (i < N && key.compareTo(array[i]) > 0) i++;if (i == N){array[N++] = key;return;}rightShift(i);array[i] = key;}private void rightShift(int insert){N++;for (int i = N-1; i >= insert+1; --i){array[i] = array[i-1];}}public T poll(){if (this.isEmpty()) return null;T ele = array[0];leftShift();return ele;}private void leftShift(){for (int i = 1; i < N; i++){array[i-1] = array[i]; }array[N-1] = null;N--;}public T peek(){return array[0];}public boolean isEmpty(){return N == 0;}@Overridepublic String toString() {if (this.isEmpty())return "[]";StringBuilder sb = new StringBuilder();sb.append('[');for (int i = 0; i < N; i++){sb.append(array[i]+", ");}String res = sb.toString().substring(0, sb.length()-2);return res + "]";}public static void main(String[] args) {int[] test = RandomUtil.randomSet(1, 100, 10);PriorityQueue<Integer> queue = new PriorityQueue<>(10);for (int i = 0; i < test.length; i++){queue.offer(test[i]);}while (!queue.isEmpty())System.out.println(queue.poll());} }

對代碼感興趣的可以研究下細節,注意,為了實現簡單,我的poll操作還是O(n),并不是達不到O(1).

堆原理

怎么說呢,上述初級實現中有一個array數組,它的結構相當低級,這也是為什么offer操作是O(n)的時間復雜度,假設你把nums一堆數offer進去,該數組在計算機看來是這樣子的:

nums = [12, 16, 37, 41, 51, 55, 56, 74, 77, 84]queue的視角: 12 -> 16 -> 37 -> 41 -> 51 -> 55 -> 56 -> 74 -> 77 ->84

可以想象,這個關系是有多么的強,我只需要開頭的最小值,結果你卻維護了整個數組的有序,而為了維護整個數組的有序,我又是rightShift,又是比較的,這耗費了多少操作?

所以歸根結底的原因在于,取最小的操作不值得維護整體的有序性,比如,我們換個視角來看問題,如下:

堆的視角:12 -> 16 -> 41 -> 74-> 37 -> 51 -> 77-> 55 -> 84-> 56該結構相當零活,在第一層的一定是最小的,而第二層的元素一定比第三層元素小,符合這種結構的解唯一么?如下:12 -> 37 -> 41 -> 74-> 16 -> 55 -> 77-> 51 -> 84-> 56同樣符合,是吧?

在這里,我們可以得到一個有趣的猜想,一種數據結構出現的結果越不“唯一”,維護該結構所需要的消耗越小。

那么現在問題來了,動態加入一個元素后,如下:

queue.offer(19);12 -> 37 -> 41 -> 74-> 16 -> 55 -> 77-> 51 -> 84-> 56

怎么加,加哪里,該如何維護?想想,如果維護的是一個樹結構,假設從根結點開始插入該元素,因為19>12,所以必然放入下一層,但放入37還是16這個結點下?無所謂,你可以放入任何一個結點的子樹中,關鍵來了!!!這就少維護了一半的關系!每次對半坎,所以說樹結構高級的原因就在于,有些操作在判斷時,都會去掉一半元素,這就好比原本規模大小為n的問題,一下子遞歸為n2的子問題,那自然而然遞歸深度只有log2n了,是吧?

所以加入一個元素如下:

12 -> 19(37) -> 41 -> 74-> 16 -> 55 -> 77-> 51 -> 84-> 56

這是堆原理的另一個關鍵步驟,我們知道了19比37來的小,所以它不可能再進入下一層,那37怎么辦?第二層已經容不下它了,所以它必須下一層找出入。這里需要注意一個堆的性質,16結點的子樹和37結點的子樹是不相關的,它們各自維護一層層的關系,所以37沒必要和16去判斷。

ok,到這堆的原理已經講完了,真正的兩個精髓被我們找到了。

  • 維護一個相對較弱的層級結構而非很強關系的有序結構,前者的維護成本要小很多。
  • 維護插入元素時,隨機挑選某個子結點進行沉降,遇到比它大的結點,把該結點的元素代入到下一層,直到葉子結點。

好了,接下來的優先隊列的實現都是細節問題了,比如為什么用數組去維護二叉堆?
答:數組就不能維護二叉堆么?隊首元素從下標1開始,所以當某個父結點為k時,左子結點為2k,右子結點為2k+1。同樣的,已知子結點k,父結點為k/2。

為什么插入是從數組尾部開始?
答:我也想從頭部開始插啊,但頭部開插,我要寫個隨機算法來隨機選擇某個子結點?而且即使實現了,你這葉子結點的生長性也太隨機了吧?隨機的結果必然導致父子結點的k求法失效。

尾插的好處是什么?
答:每次都是嚴格的擴展完全二叉堆,這還不夠好么?所以剛才是沉降操作,反過來自然有了上浮操作。

刪除了頭元素該怎么辦?
答:現在是不是就可以頭插了?畢竟沒有元素在頭,這個位置必須有元素可以替代,為了保證二叉堆的嚴格性,那肯定也是從最后一個元素取嘛,ok了,優先隊列的設計完畢了。然后,再看看圖吧,這些操作本來就是自然而然產生的。

上浮操作:

下沉操作:

再來看看具體實現吧,建議親自實現一遍,敲的時候就不要抄了,腦子過一遍,印象深刻。代碼如下:

import java.util.Comparator; import com.daimens.algorithm.utils.RandomUtil;public class PriorityQueue<Key extends Comparable<Key>> {Key[] array;int N = 0;int SIZE;private Comparator<? super Key> comparator;public PriorityQueue(int SIZE){this.SIZE = SIZE;array = (Key[]) new Comparable[SIZE+1];}public PriorityQueue(int SIZE, Comparator<? super Key> comparator){this(SIZE);this.comparator = comparator;}public void offer(Key key){array[++N] = key;swin(N);}private void swin(int k){while (k > 1 && less(k, k / 2)){swap(k, k / 2);k = k / 2;}}private boolean less(int i, int j){if (comparator != null) return comparator.compare(array[i], array[j]) < 0;else return array[i].compareTo(array[j]) < 0;}private void swap(int i, int j){Key tmp = array[i];array[i] = array[j];array[j] = tmp;}public Key poll(){Key key= array[1];swap(1, N);array[N] = null;N--;sink(1);return key;}private void sink(int k){while (2*k <= N){int j = 2 * k;if (j < N && less(j+1, j)) j++;if(!less(j, k)) break;swap(k, j);k = j;}}public Key peek(){return array[1];}public boolean isEmpty(){return N == 0;}@Overridepublic String toString() {if (this.isEmpty())return "[]";StringBuilder sb = new StringBuilder();sb.append('[');for (int i = 1; i <= N; i++){sb.append(array[i]+", ");}String res = sb.toString().substring(0, sb.length()-2);return res + "]";}public static void main(String[] args) {int[] test = RandomUtil.randomSet(1, 100, 10);PriorityQueue<Integer> queue = new PriorityQueue<>(10, (a, b) -> (b-a));for (int i = 0; i < test.length; i++){queue.offer(test[i]);}while (!queue.isEmpty()){System.out.println(queue.poll());}} }

總結

以上是生活随笔為你收集整理的算法原理系列:优先队列的全部內容,希望文章能夠幫你解決所遇到的問題。

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