硬核!手写一个优先队列
文章收錄在首發(fā)公眾號:bigsai 期待你的到訪!
前言
事情還要從一個故事講起:
對于上面那只可愛的小狗狗不會,本篇即為該教程,首先,我要告訴這只可愛的小狗狗,這種問題你要使用的數(shù)據(jù)結(jié)構(gòu)為優(yōu)先隊列,每次操作的時間復(fù)雜度為O(logn),而整個過程的時間復(fù)雜度為O(nlogn).
對于本片的設(shè)計與實現(xiàn)和堆排序可能有些相似,因為他們都借助堆來實現(xiàn)算法和數(shù)據(jù)結(jié)構(gòu),下面詳細(xì)介紹優(yōu)先隊列的設(shè)計與實現(xiàn)。
堆
而堆就是一類特殊的數(shù)據(jù)結(jié)構(gòu)的統(tǒng)稱。堆通常是一個可以被看做一棵樹(完全)的數(shù)組對象。且總是滿足以下規(guī)則:
- 堆總是一棵完全二叉樹
- 每個節(jié)點(diǎn)總是大于(或小于)它的孩子節(jié)點(diǎn)。
對于完全二叉樹,我想大家都能明白,就是最底層葉子節(jié)點(diǎn)要嚴(yán)格按照從左向右來。
堆有大根堆和小根堆,如果是所有父節(jié)點(diǎn)都大于子節(jié)點(diǎn)的時候,那么這就是個大根堆,反之則為小根堆,以下就是一個大根堆:
最后需要注意的是我們并不是用鏈?zhǔn)饺Υ孢@個二叉樹而是用數(shù)組去存儲這個樹,雖然鏈?zhǔn)降氖褂脠鼍翱赡芨嘁恍?#xff0c;但是在完全二叉樹的情況下空間使用率較好沒有斜樹的出現(xiàn)。并且在操作的時候可以直接通過編號找到位置進(jìn)行交換。
優(yōu)先隊列
如何理解優(yōu)先隊列,我們先從一段對話說起:
優(yōu)先隊列,它是一個隊列。而普通的隊列遵從先進(jìn)先出的規(guī)則。而優(yōu)先隊列遵循一個排序的規(guī)則:每次拋出自定義排序最大(小)的,默認(rèn)的情況是拋出最小的,本篇也就從最基本的原理進(jìn)行分析。
并且它的用法隊列還是一樣的,,所以我們在設(shè)計這個類的時候api方面要與隊列的api一致。
我們主要實現(xiàn)add、poll、和peek方法,并且會著重于算法的實現(xiàn)而不太著重一些細(xì)節(jié)的實現(xiàn)。
雖然優(yōu)先隊列和堆排序利用堆結(jié)構(gòu)特性的流程有一些相似,但是兩者其實還是有些操作上的區(qū)別的:
堆排序 :
- 剛開始是一個雜亂無章的序列,所以需要將雜亂的序列(樹)通過一個方法變成一個合法的堆。
- 轉(zhuǎn)成一個堆之后需要刪除n次每次刪除完都要重新調(diào)整這個堆。沒有插入操作。
優(yōu)先隊列:
- 隊列(堆)剛開始的內(nèi)容為空,每次增加一個元素時需要即使調(diào)整堆。每次刪除也要及時調(diào)整堆,增加和刪除每次都只是一個元素。
但是優(yōu)先隊列的具體操作流程是如何的呢?我們具體分析其插入和刪除的流程。
插入add流程(小根堆為例):
- 正常處理完的優(yōu)先隊列內(nèi)的數(shù)據(jù)滿足一個堆的結(jié)構(gòu),所以就是插入在堆中。
- 堆是一棵完全二叉樹,所以在插入初始,插入到最后一個位置不影響其他結(jié)構(gòu)。
- 節(jié)點(diǎn)和父節(jié)點(diǎn)比較大小(父節(jié)點(diǎn)索引為其二分之一)。如果該節(jié)點(diǎn)比父節(jié)點(diǎn)更小,則交換數(shù)據(jù),一直到不能交換為止,這個過程不用擔(dān)心不合法,因為父節(jié)點(diǎn)更小的話更滿足比孩子節(jié)點(diǎn)更小。
刪除pop流程(小根堆為例):
- pop刪除操作取優(yōu)先隊列內(nèi)最小的元素,而這個元素肯定就是堆頂元素了,取完之后,這個堆的其他部分還是滿足堆的結(jié)構(gòu)但是缺少堆頂。
- 為了不影響整個結(jié)構(gòu),我們將末尾的那個元素移到堆頂,此時堆需要調(diào)整使其滿足堆的性質(zhì)條件。
- 交換的這個節(jié)點(diǎn)和左右孩子進(jìn)行比較,如果需要交換則交換,交換后再次考慮交換子節(jié)點(diǎn)是否需要交換,一直到不交換為止。最壞情況是交換到根節(jié)點(diǎn),這個復(fù)雜度每次為O(logn).
代碼實現(xiàn)
我想到這里,優(yōu)先隊列的內(nèi)部流程思想你已經(jīng)掌握了,但是懂優(yōu)先隊列原理和手寫優(yōu)先隊列是兩個概念,為了更深入的學(xué)習(xí)優(yōu)先隊列,在這里就帶你手寫一個簡易型的優(yōu)先隊列。
在代碼的具體實現(xiàn)方面,最主要的就是pop()和add()兩個函數(shù)了。在pop()函數(shù)具體實現(xiàn)的時候,將最后一個元素移到堆頭考慮和其他孩子節(jié)點(diǎn)交換的時候,用while進(jìn)行操作的時候計算孩子下標(biāo)的時候要確保不越界。我們用的是數(shù)組存儲數(shù)據(jù),優(yōu)先隊列的長度不一定等于這個數(shù)組的長度。
而在實現(xiàn)add()函數(shù)的時候,這里簡單的考慮了一下擴(kuò)容。
具體實現(xiàn)的代碼為:
import java.util.Arrays;public class priQueue {private int size;//優(yōu)先隊列的大小private int capacity;//數(shù)組的容量private int value[];//儲存的值public priQueue() {this.capacity = 10;this.value = new int[capacity];this.size=0;}public priQueue(int capacity) {this.capacity = capacity;this.value = new int[capacity];this.size=0;}/*** 插入元素* @param number*/public void add(int number) {if(size==capacity)//擴(kuò)容{capacity*=1.5;value= Arrays.copyOf(value,capacity);}value[size++]=number;//先加到末尾int index=size-1;while (index>=1) {//進(jìn)行交換if(value[index]<value[index/2]) {swap(index,index/2,value);index=index/2;}else//不需要交換即停止break;}}public int peek() {return value[0];}/*** 拋出隊頭* @return*/public int pop() {int val=value[0];//呆返回數(shù)據(jù)額value[0]=value[--size];//將最后一個元素賦值在堆頭int index=0,leftChild=0,rightChild=0;while (true){leftChild=index*2+1;rightChild=index*2+2;if(leftChild>=size)//左孩子必須滿足在條件內(nèi)break;else if(rightChild<size&&value[rightChild]<value[index]&&value[rightChild]<value[leftChild]){//右孩子更小swap(index,rightChild,value);index=rightChild;}else if(value[leftChild]<value[index]){//左孩子更小swap(index,leftChild,value);index=leftChild;}else //不需要 它自己最小break;}return val;}//交換兩個元素public void swap(int i,int j,int arr[]) {int team=arr[i];arr[i]=arr[j];arr[j]=team;}public int size() {return size;} }寫個類測試一下看看:
結(jié)語
本次優(yōu)先隊列介紹就到這里啦,感覺不錯記得點(diǎn)贊或一鍵三連哦,建議和堆排序一起看和學(xué)習(xí)效果更佳,要能夠手寫代碼。個人公眾號:bigsai 回復(fù) bigsai 更多精彩和資源與你分享。
總結(jié)
以上是生活随笔為你收集整理的硬核!手写一个优先队列的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Leetcode 40组合总数(回溯)Ⅱ
- 下一篇: LeetCode 43字符串相乘44通配