堆與優先順序佇列的研究
堆,是一個很有意思的資料結構。邏輯結構是樹,一般為二叉樹,每個節點的值都大於(小於)其子樹中的任意節點。也就是說,使用堆結構的陣列中,元素 是部分有序的。而正是這個特點,使得在堆上,得到最大值(最小值)的時間複雜度為O(1),移除最大值(最小值)、插入元素、改變元素值(或者是刪除位置 已知的元素)的時間複雜度為O(lgn)。另外,用堆結構的排序是一種原地的、時間複雜度為O(nlgn)的排序演算法。
在優先順序佇列前先說一下堆排序。
堆排序和歸併排序都是時間複雜度為O(nlgn)的排序演算法,不同的是,歸併排序不是原地排序,它需要額外的n的空間;而堆排序是在原陣列中進行的。
堆排序的過程,首先需要在給定的陣列上進行建堆,然後在此基礎上一個一個的取根節點放到陣列的後部,從後向前排。
建堆的過程BuildHeap 程式碼很簡單,從下到上對每個節點依次處理,處理中呼叫了另一個方法:Heapify ,這個才是核心(具體參見完整程式碼中的私有BuildHeap(void) 方法與Heapify(ItemType items[], int index, int count, bool isMaxHeap) 方法);Heapify 方法對指定節點的子樹進行堆化,具體地說,當除了根節點以外,其子樹中的所有節點都已經符合堆的要求時,Heapify 方 法會把指定的節點的子樹調整為一個堆,也就是把指定節點調整到子樹中合適的位置上,其時間複雜度為子樹的深度,O(lgn)。所以,可以算出建堆的時間復 雜度為O(n),是線性的。這個時間複雜度怎麼得到的?n長度的樹,有lgn層,第h層(從0開始)至多有2^h的節點,進行一下求和就行了。
堆建好了,下來就是排序(具體可以見完整程式碼中私有的SortTo(ItemType destitionArray[]) 方法)。因為堆的根節點是最值,所以只需要依次把根節點和堆的最後一個元素交換位置,堆大小-1,再對根節點呼叫Heapify 就可以了。時間複雜度也就是n個Heapify ,O(nlgn)。
這樣從而得到總的排序時間複雜度O(nlgn)。
優先順序佇列,目的是從佇列中取出的一個元素總是當前佇列中的最大值(最小值)。而堆剛好具有這個特點。當然,有序的ArrayList或者LinkedList也是可以的,但是慢,這個後面分析。
看一下優先順序佇列都需要哪些操作。第一,其根本目的是能取出一個最值元素,這個就不用說了;第二,要能往佇列中插入元素。這是兩個核心的功能,另帶一些其他的輔助功能,如改變其中的某個元素的值,刪除某個元素,得到其中的所有元素,得到其中元素個數等等。
給出優先順序佇列的類的介面:
Remove() 方法是得到並移除第一個元素,Get() 僅僅是返回第一個元素。這個優先順序佇列用堆實現,從而可以得到主要方法Get() (O(1))、Remove() (O(lgn))、Insert()(O(lgn))較好的效能。具體實現參見完整程式碼。
Heapify 方法書上給的是個遞迴的,我給改成迭代的了,避免函式呼叫帶來的些許開銷,從而可以提高一點內隱的時間與空間的效能。
那麼為什麼用堆實現優先順序佇列比較合適呢?有序的線性表呢?
這個不難想,對於線性表,基於陣列實現:首先,如果無序,要Get() 、要Remove() ,肯定要找到個最值,如果每次都去重新搜尋,就很慢了,而且陣列牽扯到資料的整體移動(O(n));而保持有序,雖然Get() 、Remove() 快,但是Insert(ItemType value) 費的功夫有些多,因為堆中只是部分有序,而陣列需要保持整體有序,又是資料整體移動(O(n))。
基於連結串列的呢?連結串列的修改是很快,但連結串列如何快速搜尋與定位?建索引?-_-!
扯回來,說些關於具體實現時的問題。
當然,Change(int index, ItemType value) 與Remove(int index) 兩個方法在實際中是不實用的,或者是不現實的。因為通常情況下,不知道某個元素的index 是多少。通常要麼就不實現,要麼需要搜尋,或者有類似索引的機制。
還有,實際使用中佇列中存放的元素很可能是個什麼東西的指標,而不是一個沒什麼用的具體數值,這樣就還需要進行改進。
一種方式是原有的類不動,用一個實現了比較運算子的類或結構體的包裝類包裝一個有實際意義的東西,用這個型別當作PriorityQueue 的模板引數。
或者是改造PriorityQueue ,把模板引數ItemType 改成一個結構體或類,思想和上面的相似,不過這樣做功能會強大許多。比如新增一個這樣的內部類來替代模板引數ItemType :