堆排序是一種集合了插入排序與歸併排序的優點的排序演算法,即有不錯的漸近運算上限,又不用佔用額外的執行空間。簡單的說,它的排序思想如下:從一個陣列中選出最大的數,然後在剩餘的數裡選出最大的數,如此迴圈,直到陣列被窮盡,即可得到有序的陣列。
根據這個思路,很容易想到其複雜度:第一步,從n個數裡選出最大的數,需要比較n-1次;第二步,從n-1個數裡選出最大的數,比較n-2次……那麼總共需要比較的次數為:(n-1)+(n-2)+(n-3)+...+2 = O(n^2)。這與插入排序的複雜度並無區別。
而堆排序之所以能夠打破這個界限,關鍵在於它引用的“二叉堆”的概念。
所謂“二叉堆”,就是將一個陣列,按照從左到右,從上到下的順序,將每個元素填入一個二叉樹所形成的資料結構。
【此處應有示意圖】
接著就可以引入很重要的“最大堆”的概念:“最大堆”表示這樣一個二叉堆:任意節點的值總不小於其子節點的值。
而堆排序的主要步驟可以分為三步:
1 將給定的陣列經過變換得到一個最大堆。
2 將最大堆的根節點(即最大的數)與末尾的數互換,然後對除了最後一個節點以外的新二叉堆進行維護以形成一個新的最大堆。
3 對剩下的數重複第二步,直到將陣列窮盡為止。
第2,3步也就是上面所講的排序思想的實現。而堆排序的關鍵在於,經過了第一步的調整之後,接下里你從n個數裡找出最大的數不需要再比較n次,而只需要比較lg n次即可。因此第2,3步只需n lg n次運算即可結束。同時可以證明,將任意陣列重構成一個最大堆只需要O(n)的執行時間,因此總的執行時間為O(n lg n)。
演算法的實現首先需要構架一個維護最大堆的演算法heapBuilder,它的作用在於,如果一個節點的左右子樹均為最大堆,這個演算法將調整這個節點的位置,使得包括這個節點在內的新的二叉堆成為一個最大堆。
第二步構建一個實現二叉堆的演算法buildHeap,這個演算法通過由下至上,對陣列的每個元素執行heapBuilder過程,可以使任意陣列成為一個最大堆。
這樣就完成了第一步,接下來只需要構建一個完成第三步的過程即可。
以下是簡單的JS實現:
function heapBuilder(arr, i){ var left = 2*i+1; var right = 2*i+2; var largest; if(arr[left]>arr[i]&&arr[left]!=undefined) largest = left; else largest = i; if(arr[right]>arr[largest]&&arr[right]!=undefined) largest = right; if(largest != i) { [arr[i],arr[largest]] = [arr[largest],arr[i]]; heapBuilder(arr, largest); } return arr; } function buildHeap(arr){ var bound = Math.floor(arr.length/2); for(var i=bound;i>=0;i--) heapBuilder(arr, i); return arr; } function final(arr){ var temp = buildHeap(arr); var result = []; for(i=temp.length;i>0;i--){ [temp[i-1],temp[0]] = [temp[0],temp[i-1]]; result.push(temp.pop()); heapBuilder(temp, 0); } return result; }
堆的概念的引入,提供了一種“尋找某個陣列中最大(小)元素的“。即首先進行一次O(n)的整理,然後再進行O(lg n)次比較即可找出最大(小)數。顯然,若是隻需要進行常數次查詢,這樣的方式顯然多餘,但是如果需要多次查詢,乃至於第二步查詢的時間超過了O(n),那麼採用這種方法就可以極大的減少時間複雜度。
待續……