一、物理結構和概念結構
學習堆必須明確,堆有兩個結構,一個是真實存在的物理結構,一個是有助於理解的概念結構。
1. 堆一般由數組實現,但是我們平時在理解堆的時候,會把他構建成一個完全二叉樹結構。堆分為大根堆和小根堆:大根堆,就是這顆樹里的每一個結點都是以它為根結點的樹中的最大值;小根堆則與之相反。
(注意一定要是完全二叉樹)
2. 物理結構:從 0 開始的數組。
怎么將數組和二叉樹聯系起來呢?
當一個結點在數組中的下標為 index,那么這個結點對應的父節點的下標為 ( index-1 ) / 2,左孩子的下標為 2 * index +1 ,右孩子的下標為 2 * index +2 。
上面是以 0 開始的數組中各結點對應的關系,數組也可以以 1 開始,此時父節點下標為 index / 2,左孩子下標為 2 * index,右孩子下標為 2 * index + 1。
有一個物理數組下標從 0 - 8
樹結構為:
二、heapInsert
當數組中 0 ~ index -1 的位置已經是大根堆,現在添加一個元素到下標為 index ,需要怎么做才能繼續保持大根堆的結構呢?
1. 將新增元素index 與 父節點 ( index-1 ) / 2 比較,若比父節點大,則與父節點交換位置;
2. 交換位置后,新增元素下標變為 父節點的下標,再與現在這個節點的父節點比較,周而復始;
3. 直至 新增節點不再比父節點大或者已經到達了根結點,則新增節點的插入位置確定
例子:現在有一個已經在 0 ~ 7 形成大根堆的數組 [ 24, 18, 20, 10, 9, 17, 8, 5 ] ,在下標為 8 的位置插入元素 22.
JAVA 實現:
public static void heapInsert(int[] arr, int index) { // 停止條件1:新增結點不再比父節點大 // 停止條件2:已經到達了整棵樹的 根結點 0 ,當 index = 0,( 0-1)/2 =0,所以arr[index] 和 arr[(index - 1) / 2] 相等 while (arr[index] > arr[(index - 1) / 2]) { swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } }
public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
三、heapify
當將最大值 pop 出去之后,需要對這個堆進行調整,最常用的就是,將堆結構中最后一個的數提到 0 下標,然后將這個數從 0 開始下沉。
某個數在 index 位置,看是否可以往下沉。這就是 heapify。
當index 還有孩子節點時,比較左右兩個節點的大小,選取節點值較大的一個,與index進行比較 ,若 子節點的值較大,父節點下沉,較大孩子上來。直至比孩子節點大或者沒有孩子節點。
JAVA 實現:
public static void heapify(int[] arr, int index, int heapSize) { int left = index * 2 + 1;// 左孩子的下標 while (left < heapSize) {// 下方還有孩子的時候 // 兩個孩子中,誰的值大,把下標給 largest變量 int largest = (left + 1 < heapSize) && (arr[left + 1] > arr[left]) ? left + 1 : left; // 父與較大的孩子之間,誰的值大,吧下標給 largest largest = arr[largest] > arr[index] ? largest : index; if (index == largest) { break; } swap(arr, index, largest); index = largest; left = index * 2 + 1; } }
四、堆排序(非遞減)
堆排序一共分為兩步,第一步是將數組構建成一個大根堆,第二步將堆結構中的根結點,也就是最大值,移到堆的最后,然后將這個結點從堆中移除。
1. 將數組構建成一個大根堆。有兩種方法,但是這兩種方法會有不同的事件復雜度。
(1)使用 heapInsert。index 從 1 開始(因為從 0 開始的話,只有一個元素,沒有必要),直至最后一個元素 arr.length -1 ,每個元素都使用 heapInsert 的方式,逐個形成 [ 0,..., index] 的大根堆.
for (int index = 1; index < arr.length; index++) {// O(N) heapInsert(arr, index);// O(logN) }
每個元素最多向上進行 log2(N)次比較和交換,一共有 N 各元素,所以需要 O(NlogN) 的事件復雜度。
這種方法比較適合逐個向數組中添加元素,但是此時排序,傳進來的是整個數組,所以我們有一種可以將事件復雜度降低為 O(N) 的方式。
(2)使用 heapify。從最后一個元素開始,不斷下沉,使得以這個元素為根結點的數形成堆結構。
為什么說事件復雜度為 O(N) 呢?
Java 實現:
for (int index = arr.length - 1; index >= 0; index--) { heapify(arr, index, arr.length); }
2. 將堆結構中的根結點,也就是最大值,移到堆的最后,然后將這個結點從堆中移除。
將堆中最大值逐個放在 arr.length-1,arr.length-2,...,1 的位置。使用 heapSize 記錄堆中元素得個數,將堆中最大值往后,其實就是和堆中最后一個元素(下標為 heapSIze-1)交換位置,此時最后一個元素到達根結點 0 。交換完成后 heapSize--,意味着這個元素已經排序完成並將它從堆中移除,再調用 heapify 將提到根結點 0 得元素下沉。事件復雜度為 O(logN)
int heapSize = arr.length;// 記錄堆中元素的個數,如果一個數排序完成(放在堆數組的最后),就將他從堆數組中除去(heapSize--); swap(arr, 0, --heapSize);// 將堆中的最大值放在數組的最后,此時最大值排序完成,堆數組的個數減一 while (heapSize > 0) {// O(N) heapify(arr, 0, heapSize);// O(logN) swap(arr, 0, --heapSize);// O(1) }
形成堆結構,我們采用第二種方法,所以最后堆排序的代碼為:
// 堆排序 public static void heapSort(int[] arr) { if (arr == null || arr.length < 2) { return; } // 形成大根堆: // O(N) for (int index = arr.length - 1; index >= 0; index--) { heapify(arr, index, arr.length); }
// 將堆中最大值逐個放在 arr.length-1,arr.length-2,...,1 的位置 // O(N*logN) int heapSize = arr.length; swap(arr, 0, --heapSize); while (heapSize > 0) {// O(N) heapify(arr, 0, heapSize);// O(logN) swap(arr, 0, --heapSize);// O(1) } }