11.堆和堆排序&TopK

堆是一種特殊的完全二叉樹

  • 完全二叉樹即除了最後一層其它層都是滿的,且最後一層的數據全部靠左排列。
  • 特殊在,他的每個節點的值都大於等於(或者小於等於)其子樹節點,因此堆又分爲大頂堆和小頂堆。

因爲是完全二叉樹,我們存儲堆的時候一般使用數據來存儲,第一個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的局面會形成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章