Precious time, which cannot be recovered once lost.
寶貴的時間,一旦失去就無法挽回。
重複造"輪子"是大忌,但是不造"輪子",最起碼得了解"輪子"的內部結構。堆是一種特殊的樹(完全二叉樹)。本地主要分享了堆的實現原理,基於堆的排序以及堆的幾個應用。所有原始碼均已上傳至github:連結
準備
堆的定義:
- 必須是一個完全二叉樹
- 堆中每一個節點的值都必須大於等於(或小於等於)其子樹中每個節點的值。
大頂堆:
對於每個節點的值都大於等於子樹中每個節點值的堆。
小頂堆:
對於每個節點的值都小於等於子樹中每個節點值的堆。
ps:堆化是堆的核心思想,想徹底瞭解堆排序,只需要學會堆化即可。
實現一個大頂堆(小頂堆的實現在github上)
初始化
從下標1開始儲存資料 a[0]相當於一個哨兵,本文實現中,暫時未用到a[0],只是為了便於計算左節點,使堆的左節點都是2n,右節點是2n+1。要不然從a[0]開始儲存資料計算你左節點會多一次加法運算。
private MinHeap(int capacity) {
arrays = new int[++capacity];
size = capacity;
count = 0;
}複製程式碼
插入
這裡涉及到一個堆化的問題。while迴圈不斷得比較插入的資料和之前堆中的資料大小。滿足條件則進行交換。
private void insert(int data) {
if (count >= size) return;
arrays[++count] = data;
int i = count;
while (i / 2 > 0 && arrays[i] < arrays[i / 2]) {
swap(arrays, i, i / 2);
i = i / 2;
}
}複製程式碼
刪除
為什麼堆也叫優先順序佇列呢,也正因為它也是有線性操作的,刪除只能刪除堆頂元素。
這是 arrays[1] = arrays[count];是刪除堆頂元素前,先取最後一個元素塞入堆頂,然後進行堆化,防止出現陣列空洞。
private void removeMin() {
if (count == 0) return;
arrays[1] = arrays[count];
--count;
heapify(arrays, count, 1);
}複製程式碼
堆化有兩種:自下向上堆化和自上向下堆化。這裡以自上向下堆化為例。
private void heapify(int[] arrays, int count, int i) {
while (true) {
int max = i;
if (i * 2 < count && arrays[i] > arrays[i * 2]) max = i * 2;
if (i * 2 + 1 <= count && arrays[max] > arrays[i * 2 + 1]) max = i * 2 + 1;
if (max == i) break;
swap(arrays, i, max);
i = max;
}
}複製程式碼
交換滿足條件的元素
private void swap(int[] arrays, int p, int q) {
int temp = arrays[p];
arrays[p] = arrays[q];
arrays[q] = temp;
}複製程式碼
測試程式碼
public static void main(String[] args) {
MinHeap minHeap = new MinHeap(10);
for (int i = 10; i > 0; --i) {
minHeap.insert(i);
}
System.out.println("小頂堆:");
minHeap.printAll();
System.out.println("刪除堆頂元素後:");
minHeap.removeMin();
minHeap.printAll();
}複製程式碼
測試結果
堆排序
建堆
先將一個無序陣列自上向下堆化,從前往上處理資料變成一個堆。
時間複雜度O(n)
private void buildHeap(int[] arrays, int size) {
for (int i = size / 2; i > 0; --i) {
heapify(arrays, size, i);
}
}複製程式碼
排序
類似於刪除堆頂元素,然後再將這個堆(size-1)再堆化,利兩兩比較,不停的交換元素,以此類推。
時間複雜度O(nlogn)
private void sort() {
buildHeap(arrays, count);
System.out.println("建堆後:");
printAll();
int i = count;
while (i > 1) {
swap(arrays, 1, i--);
heapify(arrays, i, 1);
}
}複製程式碼
測試程式碼
maxHeap.sort();
System.out.println("排序後:");
maxHeap.printAll();複製程式碼
測試結果
求一組動態資料集合的最大 Top K
思考
針對動態資料,如何在一個包含 n 個資料的陣列中,查詢前 K 大資料呢?可以維護一個大小為 K 的小頂堆,順序遍歷陣列,從陣列中取出取資料與堆頂元素比較。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,則不做處理,繼續遍歷陣列。這樣等陣列中的資料都遍歷完之後,堆中的資料就是前 K 大資料了。
topK
這裡為了更簡便一些,用到了java PriorityQueue (優先佇列的完全體)
- 首先宣告一個大小為k的優先佇列PriorityQueue,然後迴圈arrays陣列,先新增k個數(理解這k個數已經滿足要求了)
- 其次在[k+1,arrays.length]這個範圍內,開始逐個與小頂堆的堆頂進行比較,滿足條件則交換
- 最後將PriorityQueue的資料以此取出即可
offer 新增一個元素並返回true 如果佇列已滿,則返回false
poll 移除並返問佇列頭部的元素 如果佇列為空,則返回null
peek 返回佇列頭部的元素 如果佇列為空,則返回null
private int[] topK(int[] arrays,int k){
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k);
for (int i = 0; i < arrays.length; i++) {
if (priorityQueue.size() < k){
priorityQueue.offer(arrays[i]);
}else{
int value = priorityQueue.peek();
if (arrays[i] > value){
priorityQueue.poll();
priorityQueue.offer(arrays[i]);
}
}
}
int[] result = new int[k];
int index = 0;
while (!priorityQueue.isEmpty()){
result[index++] = priorityQueue.poll();
}
return result;
}複製程式碼
測試程式碼
public static void main(String[] args) {
GetTopKArrays getTopKArrays = new GetTopKArrays();
int[] arrays = new int[10];
for (int i = 0; i <10 ; i++) {
arrays[i] = i;
}
System.out.println("一組動態資料:");
getTopKArrays.print(arrays);
int k = 3;
int[] result = getTopKArrays.topK(arrays,k);
System.out.println("前三的資料為:");
getTopKArrays.print(result);
}
}複製程式碼
測試結果
求一組動態資料集合的中位數
維護兩個堆,一個大頂堆,一個小頂堆。大頂堆中儲存前半部分資料,小頂堆中儲存後半部分資料,且小頂堆中的資料都大於大頂堆中的資料。
初始化
public Median(int capacity) {
maxHeap = new MaxHeap(capacity);
minHeap = new MinHeap(capacity);
count = 0;
}複製程式碼
插入
- 堆空,插入大頂堆
- data比大頂堆元素大插入小頂堆,否則插入大頂堆
- 如果大頂堆(小頂堆)的資料數量超過了中間值,則需要平衡,移動資料數量多的堆。
private void insert(int data) {
++count;
if (maxHeap.count == 0 && minHeap.count == 0) {
maxHeap.insert(data);
return;
}
int max = maxHeap.removeMax();
if (data > max) {
maxHeap.insert(max);
maxHeap.insert(data);
} else {
maxHeap.insert(max);
minHeap.insert(data);
}
int mid = count / 2;
if (maxHeap.count > mid) {
moveMaxMin(maxHeap, minHeap, maxHeap.count - mid);
}
if (minHeap.count > mid) {
moveMinMax(maxHeap, minHeap, minHeap.count - mid);
}
}複製程式碼
移動
因為這裡是基於上面的堆的實現,沒有封裝徹底,因此手動實現
private void moveMaxMin(MaxHeap maxHeap, MinHeap minHeap, int index) {
for (int i = 0; i < index; i++) {
int data = maxHeap.removeMax();
minHeap.insert(data);
}
}
private void moveMinMax(MaxHeap maxHeap, MinHeap minHeap, int index) {
for (int i = 0; i < index; i++) {
int data = minHeap.removeMin();
maxHeap.insert(data);
}
}複製程式碼
測試程式碼
public static void main(String[] args) {
int capacity = 10;
Median median = new Median(5);
for (int i = 1; i < capacity; i++) {
median.insert(i);
}
System.out.println("大頂堆:");
median.maxHeap.printAll();
System.out.println("小頂堆:");
median.minHeap.printAll();
int midNum = median.maxHeap.removeMax();
System.out.println("中位數為:" + midNum);
}複製程式碼
測試結果
end
您的點贊和關注是對我最大的支援,謝謝!