常見排序之堆排序
文章目錄
前言
排序演算法的成本模型計算的是比較和交換的次數。less()方法對元素進行比較,exch()方法將元素交換位置。
private static boolean less(Comparable v, Comparable w) {
return (v.compareTo(w) < 0);
}
private static void exch(Comparable[] a, int i, int j) {
Comparable swap = a[i];
a[i] = a[j];
a[j] = swap;
}
堆的定義
堆的某個節點的值總是大於等於子節點的值,並且堆是一顆完全二叉樹。當這棵樹的每個結點都大於等於它的兩個子節點時,它被稱為堆有序。
堆可以用陣列來表示,這是因為堆是完全二叉樹,而完全二叉樹很容易就儲存在陣列中。位置 k 的節點的父節點位置為 k/2,而它的兩個子節點的位置分別為 2k 和 2k+1。
上浮
在堆中,當一個節點比父節點大,那麼需要交換這個兩個節點。交換後還可能比它新的父節點大,因此需要不斷地進行比較和交換操作,把這種操作稱為上浮。
實現如下:
private void swim(int k) {
while (k > 1 && less(k/2, k)) {
exch(k, k/2);
k = k/2;
}
}
下沉
在堆中,當一個節點比子節點小,需要不斷地向下進行比較和交換操作,把這種操作稱為下沉。一個節點如果有兩個子節點,應當與兩個子節點中最大那個節點進行交換。
實現如下:
private void sink(int k) {
while (2*k <= N) {
int j = 2*k;
if (j < N && less(j, j+1)) j++;
if (!less(k, j)) break;
exch(k, j);
k = j;
}
}
堆排序
堆排序可以分為兩個階段。在堆的構造階段中,我們將原始陣列重新組織安排進一個堆中;然後在下沉排序階段,我們從堆中按遞減順序取出所有元素並得到排序結果。
堆的構造
無序陣列建立堆最直接的方法是從左到右遍歷陣列進行上浮操作。一個更高效的方法是從右至左進行下沉操作,如果一個節點的兩個節點都已經是堆有序,那麼進行下沉操作可以使得這個節點為根節點的堆有序。葉子節點不需要進行下沉操作,可以忽略葉子節點的元素,因此只需要遍歷一半的元素即可。
下沉排序
堆排序的主要工作都是在這一階段完成的。這裡我們將堆中的最大元素刪除,然後放入堆縮小後陣列中空出的位置。
public class Heap {
public static void sort(Comparable[] pq) {
int n = pq.length;
for (int k = n/2; k >= 1; k--)
sink(pq, k, n);
while (n > 1) {
exch(pq, 1, n--);
sink(pq, 1, n);
}
}
private static void sink(Comparable[] pq, int k, int n) {
while (2*k <= n) {
int j = 2*k;
if (j < n && less(pq, j, j+1)) j++;
if (!less(pq, k, j)) break;
exch(pq, k, j);
k = j;
}
}
private static boolean less(Comparable[] pq, int i, int j) {
return pq[i-1].compareTo(pq[j-1]) < 0;
}
private static void exch(Object[] pq, int i, int j) {
Object swap = pq[i-1];
pq[i-1] = pq[j-1];
pq[j-1] = swap;
}
}
複雜度分析
一個堆的高度為 logN,因此在堆中插入元素和刪除最大元素的複雜度都為 logN。
對於堆排序,由於要對 N 個節點進行下沉操作,因此複雜度為 NlogN。
現代系統的許多應用很少使用它,因為它無法利用快取。陣列元素很少和相鄰的其它元素進行比較,因此無法利用區域性性原理,快取未命中的次數很高。
- 最壞時間複雜度 О(nlogn)
- 最優時間複雜度 O(nlogn)
- 平均時間複雜度 O(nlogn)
- 空間複雜度 O(1)
- 不穩定