堆是一種特殊的樹,只要知足如下兩點,這個樹就是一個堆。算法
①徹底二叉樹,徹底二叉樹要求除了最後一層,其餘層的節點個數都是滿的,最後一層的節點都靠左排列。api
②樹中每個結點的值都必須大於等於(或小於等於)其子樹中每一個節點的值。大於等於的狀況稱爲大頂堆,小於等於的狀況稱爲小頂堆。數組
徹底二叉樹適合用數組來存儲,由於數組中對於下標從1開始的狀況,下標爲i的節點的左子節點就是下標爲i*2的節點,右子節點就是i下標爲i*2+1的節點,其父節點時下標爲i/2的節點緩存
往堆中插入一個元素數據結構
把新插入的元素放到堆的最後就不符合第二個特性了,因此咱們須要進行調整,讓其從新知足堆的特性,這個過程咱們起了一個名字,就叫做堆化(heapify)。性能
堆化就是順着節點所在的路徑,向上或者向下,對比,而後交換。咱們先使用從下往上的堆化方法。測試
讓新插入的節點與父節點對比大小。若是不知足子節點小於等於父節點的大小關係,咱們就互換兩個節點。一直重複這個過程,直到父子節點之間知足剛說的那種大小關係。ui
public class Heap{ private int[] data;//數組,從下標1開始存儲 private int maxNum;//數組容量 private int count;//當前數組成員數量 //構造器初始化數組,大小和數量 public Heap(int size){ data = new int[size + 1]; maxNum = size; count = 0; } public void Insert(int item){ //堆滿返回 if (count >= maxNum) return; //先將節點插入堆尾 data[count++] = item; int i = count; //再自下向上堆化,直到堆頂或者父節點比子節點大爲止 while (i / 2 > 0 && data[i] > data[i / 2]){ //交換位置 int temp = data[i]; data[i] = data[i / 2]; data[i / 2] = temp; //更新下標 i = i / 2; } } }
刪除堆頂元素spa
根據對的第二條定義,堆頂元素存儲的就是堆中的最大值或最小值。code
這裏咱們使用從上往下的堆化方法。將最後一個節點放到堆頂,而後利用一樣的父子節點對比法,進行互換節點直到父子節點之間知足大小關係爲止。
這樣移除的就是數組中的最後一個元素,不會破環徹底二叉樹的定義。
public void RemoveMax(){ //堆空返回 if (count == 0) return; //將最後一個節點提到堆頂 data[1] = data[count--]; //進行堆化 Heapify(data,count,1); } public static void Heapify(int[] data,int n,int i){ while (true){ //記錄更大節點的位置,初始化爲當前節點的位置 int maxPos = i; //若是其左右子節點存在,且比當前節點大,就將左右節點下標設爲更大的節點 if (i * 2 <= n && data[i] < data[i * 2]) maxPos = i * 2; if (i * 2 + 1 <= n && data[maxPos] < data[i * 2 + 1]) maxPos = i * 2 + 1; //不然就結束循環,堆化結束 if (maxPos == i) break; //節點交換位置 int temp = data[i]; data[i] = data[maxPos]; data[maxPos] = temp; //更新當前節點的下標,循環繼續與下一個左右子節點比較 i = maxPos; } }
咱們藉助於堆這種數據結構實現的排序算法,就叫做堆排序。
咱們能夠把堆排序的過程大體分解成兩個大的步驟,建堆和排序。
首先將數組原地建成一個堆。藉助另外一個數組,就在原數組上操做。咱們要實現從後往前處理數組,而且每一個數據都是從上往下堆化的建堆方法。
public static void BuildHeap(int[] data, int n){ //從下標n/2到1開始進行堆化,n/2就是最後一個葉子節點的父節點。 for (int i = n / 2; i >= 1; --i) Heapify(data,n,i); }
咱們對下標從n/2開始到 111 的數據進行堆化,下標是n/2+1到n的節點是葉子節點,咱們不須要堆化。
建堆操做的時間複雜度
排序的建堆過程的時間複雜度是 O(n)。
建堆結束以後,數組中的數據已是按照大頂堆的特性來組織的。數組中的第一個元素就是堆頂,也就是最大的元素。咱們把它跟最後一個元素交換,那最大元素就放到了下標爲n的位置。
這個過程有點相似刪除堆頂元素的操做,當堆頂元素移除以後,咱們把下標爲n的元素放到堆頂,而後再經過堆化的方法,將剩下的n-1個元素從新構建成堆。
堆化完成以後,咱們再取堆頂的元素,放到下標是的位置,一直重複這個過程,直到最後堆中只剩下標爲1的一個元素,排序工做就完成了。
public static void Sort(int[] data,int n){ //將數組建造爲堆 BuildHeap(data, n); //獲取堆尾的下標 int k = n; //循環直到k爲1 while (k > 1){ //交換堆頂和堆尾的元素 int temp = data[k]; data[k] = data[1]; data[1] = temp; //將堆尾的下標遞減並對1到k的下標的數組成員進行堆化 Heapify(data,--k,1); } }
堆排序的時間複雜度、空間複雜度以及穩定性
堆排序是原地排序算法。堆排序包括建堆和排序兩個操做,建堆過程的時間複雜度是O(n),排序過程的時間複雜度是O(nlogn)因此,堆排序總體的時間複雜度是O(nlogn)。
堆排序不是穩定的排序算法,由於在排序的過程,存在將堆的最後一個節點跟堆頂節點互換的操做,因此就有可能改變值相同數據的原始相對順序。
//Main方法 int[] data = new int[] {0,3,5,2,9,4,7 }; Heap.Sort(data,data.Length-1); for (int i=0;i<data.Length;i++) Console.Write(data[i]+","); //測試結果 0,2,3,4,5,7,9,
數組的第1個成員,即下標0的數據是不做爲數據的一部分的,這是爲了算法上的方便,若是下標是從0開始,那麼左右子節點的下標公式就是i*2+1和i*2+2。
對於快速排序來講,數據是順序訪問的而對於堆排序來講,數據是跳着訪問的。這樣對 CPU 緩存是不友好的。
對於一樣的數據,在排序過程當中,堆排序算法的數據交換次數要多於快速排序。堆排序的第一步是建堆,建堆的過程會打亂數據原有的相對前後順序,致使原數據的有序度下降。好比,對於一組已經有序的數據來講,通過建堆以後,數據反而變得更無序了。