Heap和Heapify


最近復習數據結構,又回去再看塞神的課件,看到PriorityQueue的實現。自己也根據塞神的代碼寫一寫。

 

下面使用Binary Heap實現了一個簡單的 Max-oriented PriorityQueue。

  1. 這里Binary Heap我們使用的是array represetation,數組形式。
    1. 第0個元素我們留空,從第一個元素開始存儲, 第一個元素也將是PQ里最大的元素。
    2. 特點是假如父節點位置是 k, 那么兩個子節點的位置就是 2 * k 和 2 * k + 1。這樣很方便計算,知道父節點很容易計算出子節點,知道子節點的位置也能立刻知道父節點的位置。
    3. Max Heap服從Max heap order, 即父節點的值永遠大於等於子節點的值。           (但父節點的sibling可以不大於當前父節點的子節點值)
    4. 元素最好是implements Comparable[],否則我們還要另外寫Comparator<>()
  2. 一開始的構造方法里,我們為簡便使用了一個定長的數組。正常來說應該使用一個resizing array,以及一個load factor。
    1. 當load factor,也就是元素數 N / 數組長度 len = 0.75的時候,我們把數組擴容一倍,然后把之前的元素拷貝進去
    2. 當load factor < 0.25的時候,我們把數組減半,也要拷貝之前的元素。 
  3. 主要的一些方法有insert(),peek(), delMax()以及isEmpty(),為了測試我也放入了一些其他方法,比如shuffle(), heapify(), 和heapSort(),下面一點點來分析各個方法。
    1. insert():  每次添加元素的時候,我們都先增加PQ中元素的個數,即++N,然后把新元素x放在數組中新的這個位置上。接下來我們調用swim()方法來使PQ依然保持有序。  總復雜度是O(logn)
    2. swim():   向上維護heap order。當我們發現,或者不確定數組中一個位置的元素是否符合Max heap order時,我們需要對這個位置的值進行一個swim()操作。只用考慮子節點和父節點,不用考慮sibling.
      1. 主要操作就是將這個子節點和其父節點進行比較,假如這個位置為k的子節點的值大於其父節點,我們交換這兩個節點
      2. 繼續比較交換后的子節點和其新的父節點, 這個可以通過 k /= 2來完成。 
      3. 遍歷在 k > 1的條件下進行。因為 k > 1的時候,  k / 2最大就是1,也就是我們的最大節點
      4. 每次insert的時候我們可以使用swim()來保持heap order
    3. peek():  我們可以直接返回最大節點elements[1],注意一些邊界條件,或者這個節點不存在的時候拋出Exception
    4. delMax():  刪除最大節點是Max-heap的特色。             總復雜度 O(logn)
      1. 我們先交換最大節點和最后一個位置的節點,用N-- 將元素數N減少1, 並且將最大節點所在位置置為空 -  elements[N + 1] = null。 這樣可以避免loitering,避免垃圾回收機制收不到這個數組。
      2. 這時我們處在elements[1]位置上的元素有可能不滿足Max heap order,我們執行 sink() 方法來進行處理。    
    5. sink():  向下維護heap order。 這時我們知道這個元素有可能和其兩個子節點間都不滿足Max heap order。我們在判斷的時候要同時比較父節點和兩個子節點間的大小。
      1. 假設當前父節點位置為k,那么兩個可能的子節點位置為 2 * k 和 2 * k + 1。我們要先判斷左子節點是否存在,也就是 2 * k 是否 <= N
      2. 在左子節點存在的條件下,我們設置 j = 2 * k,接下來我們判斷右子節點是否存在,即  j是否 < N, 假如右子節點存在,我們比較左右子節點的大小,並且嘗試更新j 為較大子節點的index值
      3. 接下來我們判斷是否較大的子節點大於父節點的值,  假如為否,elements[j] < elements[k], 那么我們直接break
      4. 否則,我們交換 k 和 j -  swap(k, j), 並且更新k  = j, 繼續下一個level的sink
    6. isEmpty():  pq是否為空,這是我們直接判斷是否  N == 0
    7. swap():  交換兩個節點
    8. shuffle():  這里使用了knuth shuffle。就是先用seed建立一個Random, 然后遍歷數組的時候生成偽隨機數,與當前index進行交換。   O(n)
    9. heapify():  這里是指最大heapify。 我們只需要從  k = N / 2開始,  在k >= 1的條件下對 k 進行sink(), 然后k--就可以了。
    10. heapSort(): 堆排序, 這里我們先對數組進行heapify(), 然后在k > 1的條件下每次把最大元素交換到數組尾部,再對位置1的元素進行sink就可以了。   in-place  O(nlogn)。

 

 

 

 

 

public class MaxPQ {
    public Integer[] elements;
    public int N;
    
    public MaxPQ(int size) {
        elements = new Integer[size + 1];
        N = 0;            // index starts with 1        
    }
    
    public void insert(Integer x) {
        elements[++N] = x;
        swim(N);
    }
    
    private void swim(int k) {
        while (k > 1 && elements[k] > elements[k / 2]) {
            swap(k, k / 2);
            k /= 2;
        }
    }
    
    public Integer delMax() {
        Integer max = elements[1];
        swap(1, N--);
        elements[N + 1] = null;
        sink(1);
        return max;
    }
    
    private void sink(int k) {
        while (2 * k <= N) {
            int j = 2 * k;
            if (j < N && elements[j] < elements[j + 1]) {  
                j++;
            }
            if (elements[j] < elements[k]) {
                break;
            }
            swap(k, j);
            k = j;
        }
    }
    
    public Integer peek() {
        return elements[1];
    }
    
    public boolean isEmpty() {
        return N == 0;
    }
    
    private void swap(int i, int j) {
        Integer tmp = elements[i];
        elements[i] = elements[j];
        elements[j] = tmp;
    }
    
    public void shuffle() {            // for testing
        java.util.Random rand = new java.util.Random(System.currentTimeMillis());
        for (int i = 1; i <= N; i++) {
            int r = 1 + rand.nextInt(i);
            swap(i, r);
        }        
    }
    
    public void heapify() {            // for testing
        for (int k = N / 2; k >= 1; k--) {
            sink(k);
        }
    }
    
    public void heapSort() {
        heapify();
        int n = N;
        while (n > 1) {
            swap(1, n--);
            sink(1);
        }
    }
}

 

 

上面是用Binary heap設計一個 Max-oriented Priority Queue, 數組是1-based。 假如遇到面試官問怎么heapify怎么辦?  下面我們就對上面代碼進行少許改動,變為0-based,可以直接對數組進行max - heapify。

  1. heapify()方法: 可以看出我們的heapify方法基本沒有變化,除了把N / 2變成了數組的長度 nums.length / 2
  2. sink()方法 : 這里我們要注意一下邊界條件。 先設置len = nums.length,這里len就相當於之前的N, 然后再進行比較的時候,我們要把每次的 j 都減1,從1-based改變為 0-based,其他代碼都不需要改變

 

    public static void heapify(int[] nums) {
        if (nums == null) {
            return;
        }
        for (int k = nums.length / 2; k >= 1; k--) {
            sink(nums, k);
        }
    }
    
    private static void sink(int[] nums, int k) {
        int len = nums.length;
        while (2 * k <= len) {
            int j = 2 * k;
            if (j < len && nums[j - 1] < nums[j]) {
                j++;
            }
            if (nums[k - 1] > nums[j - 1]) {
                break;
            }
            swap(nums, k - 1, j - 1);
            k = j;
        }
    }
    
    private static void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }

 

Test Client: 

public static void main(String[] args) {
        
        int len = 10;
        int[] nums = new int[len];
        for (int i = 0; i < len; i++) {
            nums[i] = i + 1; 
        }
        
        shuffle(nums);
        
        for (int i : nums) {
            System.out.print(i + " ");
        }
        
        heapify(nums);
        System.out.println();
        
        for (int i : nums) {
            System.out.print(i + " ");
        }
    }
    

 

Reference:

http://algs4.cs.princeton.edu/24pq/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM