堆是一種特殊的完全二叉樹
- 完全二叉樹即除了最後一層其它層都是滿的,且最後一層的數據全部靠左排列。
- 特殊在,他的每個節點的值都大於等於(或者小於等於)其子樹節點,因此堆又分爲大頂堆和小頂堆。
因爲是完全二叉樹,我們存儲堆的時候一般使用數據來存儲,第一個0號元素留空,這樣的話節點是a[n],左節點就是a[2n],右節點就是a[2n+1],父節點就是a[n/2]。當然不留空也可以,推算父節點的時候總是要先進行-1操作。
堆主要有兩個操作
- 刪除堆頂元素
- 插入一個元素
這兩個操作都涉及到了堆化(重新調整,使其滿足堆的特性),刪除堆頂元素我們選擇的是把最後一個元素移到堆頂來,從上而下堆化(比較當前節點和子節點的大小,不滿足則互換位置)。插入一個元素我們選擇把元素插入到最後,然後從下往上堆化(比對當前節點和父節點的大小,不滿足則互換位置,互換後一直)。
關於堆的應用
- 計算Top K
原理:先去數據中前K個數字建立一個大小爲K的小頂堆(堆頂元素最大),然後每進來一下新數字,比對和堆頂元素的大小。如果比堆頂元素大,則刪除堆頂元素,並插入新數字,重新堆化。 - 優先級處理,比如定時任務,等待隊列等等
原理:每插入或者刪除一個新元素,重新堆化處理。堆頂元素永遠是優先級最高的一個。 - 堆排序
代碼實現
/**
* 小頂堆的實現
* 堆頂元素最小,子元素均大於堆頂元素
*/
public class Heap {
private int[] a;//數組,從下標1開始存儲數據
private int n;//堆存儲的最大個數
private int count;//目前個數
public Heap(int n) {
this.n = n;
count = 0;
a = new int[n + 1];
}
public Heap(int[] a) {
this.a = a;
count = a.length - 1;
n = count;
}
public void removeHeader() {
if (count == 0) {
return;
}
a[1] = a[count];
int current = 1;
//從上而下堆化,需要用父元素和下面兩個子節點比對
heapify(a, count, current);
}
public boolean insert(int data) {
if (count == n) {
return false;
}
count++;
a[count] = data;
int i = count;
//從下往上堆化,和父元素對比即可
while (i / 2 > 0 & a[i / 2] > a[i]) {
//父元素大於子元素
swap(a, i, i / 2);
i = i / 2;
}
return true;
}
/**
* 這個a的第一個元素應該是個空的
* 空,數字,數字,數字
* 0, 1 ,2 , 3 ,
*
* @param a
* @param n a數組中數據的個數,可能a的長度是100,數字確是6個,後93個忽略。
* @param current 給當前角標的數字尋找一個合適的位置存放堆化的a,
* current子節點的節點需要是已經滿足堆化的數據,否則該方法不適用
*/
private static void heapify(int a[], int n, int current) {
while (true) {
int minxPos = current;
if (current * 2 <= n && a[current] > a[current * 2]) {
minxPos = current * 2;
}
//這裏要用if而不是elseif,且是a[mixPos]對比,是因爲要找出兩個子節點中最小的一個
if (current * 2 + 1 <= n && a[minxPos] > a[current * 2 + 1]) {
minxPos = current * 2 + 1;
}
if (minxPos == current) {
break;
}
swap(a, current, minxPos);
current = minxPos;
}
}
/**
* 從下而上堆化
*
* @param a
* @return
*/
public static Heap buildHeap1(int[] a) {
Heap heap = new Heap(a.length);
for (int i = 0; i < a.length; i++) {
heap.insert(a[i]);
}
return heap;
}
/**
* 從上而下堆化
* 我們僅需要對下標從1到n/2的數據進行堆化,小標是n/2+1帶n的節點是葉子節點,不需要堆化。
* 從上而下堆化本就是拿父節點和兩個子節點對比,找出三者中最合適做父元素的一個值。
*
* 這種建堆方法的複雜度是O(n)
* @param b
* @return
*/
public static Heap buildHeap2(int[] b) {
int[] a = new int[b.length + 1];
for (int i = 0; i < b.length; i++) {
a[i + 1] = b[i];
}
//必須逆序,每個值動過一次之後就是最合適的位置了(最下面的沒有自節點了),不需要再動了
for (int current = a.length / 2; current >= 1; current--) {
heapify(a, b.length, current);
}
return new Heap(a);
}
}
堆排序的思想,首先我們對數據進行從下到上的建堆操作(對每個元素進行堆化)。然後取出堆頂元素和最後一個元素交換位置。然後堆化對頂元素,直到剩餘一個元素的時候,整體有序。
public class SortHeap {
public static void main(String[] args) {
int[] nums = new int[]{9, 1, 2, 3, 4, 5, 6, 7, 8};
new SortHeap().sort(nums);
}
public void sort(int[] nums) {
//多次後堆化完成
for (int i = nums.length / 2 - 1; i >= 0; i--) {
heap(nums, nums.length, i);
}
/**
* 堆頂已經排序完成
*/
for (int i = nums.length - 1; i >= 0; i--) {
int temp = nums[i];
nums[i] = nums[0];
nums[0] = temp;
//堆化第一個數字
heap(nums, i, 0);
}
//然後每次移動走一個,不是建堆,而是進行某一個堆化,只需要變動一個值的位置就可以了,
// 而不是檢測一遍
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 一個完全二叉樹,總是有(n+1)/2個子節點
* 一個滿二叉樹,總是有(n+1)/2個子節點(n總是奇數,+1變成偶數)。
* 然後追加成完全二叉樹,每當n+1的時候,子節點數量不變,因爲8/2和9/2是一樣的值
* 然後再追加一個,此時子節點數量就會+1了,還是滿足(n+1)/2個子節點,
* n是奇數的時候,有n+1/2個子節點,n爲偶數的時候,有n/2個子節點
* 所以說,n奇數的時候有n-1/2個子節點,所以說總有n/2個子節點
* 所以index角標是n/2-1
* <p>
*
* 堆化其中一個
*
* @param nums
*/
private void heap(int[] nums, int needHeapLength, int index) {
while (true) {
//這裏要一直找到nums[i]應該在的位置
int minIndex = index;
if (index * 2 + 1 < needHeapLength
&& nums[index * 2 + 1] < nums[index]) {
//設置當前minIndex的值,是樹的左下角
minIndex = index * 2 + 1;
}
if (index * 2 + 2 < needHeapLength
&& nums[index * 2 + 2] < nums[minIndex]) {
//設置當前minIndex的值,是樹的右下角
minIndex = index * 2 + 2;
}
if (minIndex == index) {
break;
}
//交換index和minIndex的位置
int num = nums[index];
nums[index] = nums[minIndex];
nums[minIndex] = num;
//一個值挪動了,接着驗證被挪動走的值的下方是否符合堆化條件,不符合繼續動
index = minIndex;
}
}
}
堆排序是不穩定排,因爲初始化後最後一個元素,可能會被和堆頂元素交換。然後接着被堆化的時候移動到前面去了。
比如:98,86,68,58,421,422(堆化之後)
然後對頂元素移動到最後:86,68,58,421,98
然後堆化第一個元素:86,68,58,422,421,98
…
依次類推,最終422,421的局面會形成。