數據結構-堆

定義

優先隊列:一種特殊的隊列,隊列中元素出棧的順序是按照元素的優先權大小,而不是元素入隊的前後順序。java

heap
heap

堆的特性:git

  • 必須是徹底二叉樹
  • 用數組實現
  • 任一結點的值是其子樹全部結點的最大值或最小值
    • 最大值時,稱爲「最大堆」,也稱大頂堆;
    • 最小值時,稱爲「最小堆」,也稱小頂堆。

最大堆
最大堆

最小堆
最小堆

能夠看到,對於堆(Heap)這種數據結構,從根節點到任意結點路徑上全部的結點都是有序的。github

堆的ADT

ADT
ADT

堆的實現

堆是用數組實現的徹底二叉樹,所以在Java中咱們可使用ArrayList實現,並且向ArrayList中插入元素時,當數組容量不足時,他會自動增加,這樣也免去考慮堆最大容量的問題。這裏重點描述以上ADT中插入和刪除的操做。通常來講,會從堆中刪除最大值,其實也就是最大堆中的第一個元素。下面的實現爲了普適性,實現了從堆中刪除任一結點的操做。算法

下面就以最大堆的構成爲例,研究一下如何使用數組實現堆。數組

最大堆

插入

堆的插入如何實現呢?只要咱們謹記的定義,實現起來實際上是很容易的。這裏在回顧一下重點bash

  1. 徹底二叉樹
  2. 任一結點的值是其左右子樹的最大值
  3. 用數組實現

考慮下圖所示的堆。markdown

假設現有元素60須要插入,爲了維持徹底二叉樹的特性,新插入的元素必定是放在結點44的右子樹;同時爲了知足任一結點的值要大於左右子樹的值這一特性,新插入的元素要和其父結點做比較,若是比父結點大,就要把父結點拉下來頂替當前結點的位置,本身則依次不斷向上尋找,找到比本身小的父結點就拉下來,直到沒有符合條件的值爲止。這樣,到最後就完成了插入操做;總結一下:數據結構

  1. 新插入的結點添加到數組最後
  2. 和其父結點比較大小,若是大於父結點,就用父結點替換當前位置,同時本身的位置上移。
  3. 直到父結點再也不大於本身或者是位置已近到了數組第一個位置,就找到屬於本身的位置了。

這裏爲了方便,咱們直接佔用了數組下標爲0的位置,在0的位置放置了一個null,這樣數組中實際有效值的下標就和咱們徹底二叉樹中層序遍歷的實際序號對應了。這樣,徹底二叉樹中,若是結點值爲n,那麼其左子樹則爲2n,右子樹爲2n+1;換句話說,對於任一結點n,其父結點爲n/2 取整便可。app

  • 初始化堆
public class MaxHeap<T extends Comparable<T>> {

    private List<T> mHeap;

    public MaxHeap() {
        mHeap = new ArrayList<>();
        // 爲了方便,數組下標爲0 的位置,放置一個空元素,使得數組從下標爲1的位置開始
        // 這樣,徹底二叉樹中,若是結點值爲n,那麼其左子樹則爲2n,右子樹爲2n+1
        mHeap.add(0, null);
    }

}複製代碼

固然,爲了保證有序性,咱們須要堆內元素實現了Comparable接口。oop

  • 插入操做
/** * 堆的插入操做 * @param value */
    public void insert(T value) {
        //新插入的元素首先放在數組最後,保持徹底二叉樹的特性
        mHeap.add(value);
        // 獲取最後一個元素的在數組中的索引位置,注意是從index=1的位置開始添加
        int index = mHeap.size() - 1;
        // 其父結點位置
        int pIndex = index / 2;



        //在數組範圍內,比較這個插入值和其父結點的大小關係,大於父結點則用父結點替換當前值,index位置上升爲父結點
        while (index > 1) {
            // 插入結點小於等於其父結點,則不用調整
            if (compare(value, mHeap.get(pIndex)) <= 0) {
                break;
            } else {
                // 依次把父結點較小的值「將」下來
                mHeap.set(index, mHeap.get(pIndex));
                // 向上升一層
                index = pIndex;
                // 新的父結點
                pIndex = index / 2;
            }
        }
        // 最終找到index 的位置,把值放進去
        mHeap.set(index, value);


    }

    /** * * @param a * @param b * @return a>b 返回值大於0,反之小於0 */
    private int compare(T a, T b) {
        return a.compareTo(b);
    }複製代碼

這裏須要注意的是,當插入結點大於父結點時,咱們並無交換兩個元素的算法,而只是把小的元素「降」了下來,由於咱們最終只是想要找到一個正確的位置而已,交換是沒必要要,只須要在最後在合適的位置把值放上去就能夠了

刪除

理解了插入的實現,刪除也是遵循一樣的規則。

、

假設要從上圖中刪除結點58,爲了維持徹底二叉樹的特性,咱們很容易想到用最後一個元素31去替代這個58;而後比較31和其子樹的大小關係,若是比左右子樹小(若是存在的話),就要從左右子樹中找一個較大的值替換他,而他能本身就要跑到對應子樹的位置,再次循環這種操做,直到沒有子樹比他小就能夠了。在這裏,按照以上的思路,44將跑到根節點的位置,而他的位置將由31替代,堆依然是堆。總結一下:

  1. 找到要刪除的結點在數組中的位置
  2. 用數組中最後一個元素替代這個位置的元素
  3. 當前位置和其左右子樹比較,保證符合最大堆的結點間規則
  4. 刪除最後一個元素
/** * 堆的任意值的刪除操做 * @param value * @return */
    public boolean delete(T value) {
        if (mHeap.isEmpty()) {
            return false;
        }
        // 獲得數組中這個元素的下標
        int index = mHeap.indexOf(value);
        if (index == -1) { // 被刪除元素不在數組中,即刪除元素不在堆中
            return false;
        }

        // 獲取最後一個元素的在數組中的索引位置,注意是從index=1的位置開始添加
        int lastIndex = mHeap.size() - 1;

        T temp = mHeap.get(lastIndex);
        // 用最後一個元素替換被刪除的位置
        mHeap.set(index, temp);


        int parent;
        for (parent = index; parent * 2 <= mHeap.size()-1; parent = index) {
            //當前結點左子樹下標
            index = parent * 2;
            // 左子樹下標不等於數組長度,所以必然有右子樹 ,則左右子樹比較大小,這裏-1 是由於數組下標=1 開始
            if (index != mHeap.size()-1 && compare(mHeap.get(index), mHeap.get(index + 1))<0) {
                // 若是右子樹大,則下標指向右子樹
                index=index+1;
            }

            if (compare(temp, mHeap.get(index)) > 0) {
                //當前結點大於其左右子樹,則不用調整,直接退出
                break;
            }else {
                // 子樹上移,替換當前結點
                mHeap.set(parent, mHeap.get(index));
            }


        }
        // parent 就是替換結點最終該處的位置
        mHeap.set(parent, temp);
        // 移除數組最後一個元素
        mHeap.remove(lastIndex);
        return true;


    }複製代碼

關於刪除操做,須要注意的一點就是,因爲咱們的數組至關因而從下標=1 的位置開始,所以須要注意數組邊界值和其長度的關係

下面就來測試一下最大堆的實現:

測試類
private static Integer[] arrays = new Integer[]{10, 8, 3, 12, 9, 4, 5, 7, 1, 11, 17};

    private static void MaxHeapTest() {
        MaxHeap<Integer> mMaxHeap = new MaxHeap<>();
        for (int i = 0; i < arrays.length; i++) {
            mMaxHeap.insert(arrays[i]);
        }

        mMaxHeap.printHeap();
        System.out.printf("delete value %d from maxHeap isSuccess=%b \n", 17, mMaxHeap.delete(17));
        mMaxHeap.printHeap();
        System.out.printf("delete value %d from maxHeap isSuccess=%b \n", 1, mMaxHeap.delete(1));
        mMaxHeap.printHeap();
        System.out.printf("delete value %d from maxHeap isSuccess=%b \n", 12, mMaxHeap.delete(12));
        mMaxHeap.printHeap();
        System.out.printf("insert value %d to maxHeap \n", 16);
        mMaxHeap.insert(16);
        mMaxHeap.printHeap();

    }複製代碼

printHeap() 的實現能夠參考如下最小堆完整源碼

輸出:

17 12 5 8 11 3 4 7 1 9 10 
delete value 17 from maxHeap isSuccess=true 
12 11 5 8 10 3 4 7 1 9 
delete value 1 from maxHeap isSuccess=true 
12 11 5 8 10 3 4 7 9 
delete value 12 from maxHeap isSuccess=true 
11 10 5 8 9 3 4 7 
insert value 16 to maxHeap 
16 11 5 10 9 3 4 7 8複製代碼

能夠看到,當咱們第一次完成遍歷插入後,將構建出以下所示的一顆徹底二叉樹,很顯然這也是最大堆。當咱們一次刪除元素或插入元素時,根據輸出結果對應的堆,能夠看到咱們的插入和刪除操做都是正確的。

畫歪的樹
畫歪的樹

這棵樹畫歪了,湊合看吧,o(╯□╰)o

後面幾個輸出對應的樹,感興趣的同窗能夠手動畫一下,學二叉樹手動畫樹真是一個好方法

最小堆

最小堆,每個結點的值都小於其左右子樹的值,所以很容易的咱們能夠想到,在構建最大樹時把全部判斷大小的邏輯取反就能夠實現了。事實上也的確就是這麼簡單,下面給出完整最小堆實現的完整代碼,就不具體分析了。

public class MinHeap<T extends Comparable<T>> {
    private List<T> mHeap;
    //堆內當前元素個數
    public int size;

    public MinHeap() {
        mHeap = new ArrayList<>();
        // 爲了方便,數組下標爲0 的位置,放置一個空元素,使得數組從下標爲1的位置開始
        // 這樣,徹底二叉樹中,若是結點值爲n,那麼其左子樹則爲2n,右子樹爲2n+1
        mHeap.add(0, null);
    }

    public void insert(T value) {
        //新插入的元素首先放在數組最後,保持徹底二叉樹的特性
        mHeap.add(value);
        // 獲取最後一個元素的在數組中的索引位置,注意是從index=1的位置開始添加,所以最後一個元素的位置是size-1
        int index = mHeap.size() - 1;
        // 其父結點位置
        int pIndex = index / 2;



        //在數組範圍內,比較這個插入值和其父結點的大小關係,小於父結點則用父結點替換當前值,index位置上升爲父結點
        while (index > 1) {
            // 插入結點大於等於其父結點,則不用調整
            if (compare(value, mHeap.get(pIndex)) >= 0) {
                break;
            } else {
                // 依次把父結點較大的值「將」下來,把小的值升上去
                mHeap.set(index, mHeap.get(pIndex));
                // 向上升一層
                index = pIndex;
                // 新的父結點
                pIndex = index / 2;
            }
        }
        // 最終找到index 的位置,把值放進去
        mHeap.set(index, value);


    }


    public boolean remove(T value) {
        if (mHeap.isEmpty()) {
            return false;
        }
        // 獲得數組中這個元素的下標
        int index = mHeap.indexOf(value);
        if (index == -1) { // 被刪除元素不在數組中,即刪除元素不在堆中
            return false;
        }

        // 獲取最後一個元素的在數組中的索引位置,注意是從index=1的位置開始添加,所以最後一個元素的位置是size-1
        int lastIndex = mHeap.size() - 1;

        T temp = mHeap.get(lastIndex);
        // 用最後一個元素替換被刪除的位置
        mHeap.set(index, temp);


        int parent;
        for (parent = index; parent * 2 <= mHeap.size()-1; parent = index) {
            //當前結點左子樹下標
            index = parent * 2;
            // 左子樹下標不等於數組長度,所以必然有右子樹 ,則左右子樹比較大小
            if (index != mHeap.size()-1 && compare(mHeap.get(index), mHeap.get(index + 1))>0) {
                // 若是右子樹小,則下標指向右子樹
                index=index+1;
            }

            if (compare(temp, mHeap.get(index)) < 0) {
                //當前結點小於其左右子樹,則不用調整,直接退出
                break;
            }else {
                // 子樹上移,替換當前結點
                mHeap.set(parent, mHeap.get(index));
            }


        }
        // parent 就是替換結點最終該處的位置
        mHeap.set(parent, temp);
        // 移除數組最後一個元素
        mHeap.remove(lastIndex);
        return true;


    }

    private int compare(T a, T b) {
        return a.compareTo(b);
    }

    public void printHeap(){
        StringBuilder sb = new StringBuilder();
        for(int i=1;i<mHeap.size();i++) {
            sb.append(mHeap.get(i)).append(" ");
        }

        System.out.println(sb.toString());
    }
}複製代碼

測試類就不在這裏佔篇幅了,有興趣的同窗能夠直接看源碼.


好了,堆的實現就到這裏了。

相關文章
相關標籤/搜索