基於"堆"的底層實現和應用

弒曉風發表於2019-03-09
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 (優先佇列的完全體)

  1. 首先宣告一個大小為k的優先佇列PriorityQueue,然後迴圈arrays陣列,先新增k個數(理解這k個數已經滿足要求了)
  2. 其次在[k+1,arrays.length]這個範圍內,開始逐個與小頂堆的堆頂進行比較,滿足條件則交換
  3. 最後將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;
    }複製程式碼

插入

  1. 堆空,插入大頂堆
  2. data比大頂堆元素大插入小頂堆,否則插入大頂堆
  3. 如果大頂堆(小頂堆)的資料數量超過了中間值,則需要平衡,移動資料數量多的堆。

    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

基於"堆"的底層實現和應用

您的點贊和關注是對我最大的支援,謝謝!


相關文章