資料結構-堆

小墨魚3發表於2020-01-31

定義

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

heap
heap

堆的特性:

  • 必須是完全二叉樹
  • 用陣列實現
  • 任一結點的值是其子樹所有結點的最大值或最小值
    • 最大值時,稱為“最大堆”,也稱大頂堆;
    • 最小值時,稱為“最小堆”,也稱小頂堆。

最大堆
最大堆

最小堆
最小堆

可以看到,對於堆(Heap)這種資料結構,從根節點到任意結點路徑上所有的結點都是有序的。

堆的ADT

ADT
ADT

堆的實現

堆是用陣列實現的完全二叉樹,因此在Java中我們可以使用ArrayList實現,而且向ArrayList中插入元素時,當陣列容量不足時,他會自動增長,這樣也免去考慮堆最大容量的問題。這裡重點描述以上ADT中插入和刪除的操作。一般來說,會從堆中刪除最大值,其實也就是最大堆中的第一個元素。下面的實現為了普適性,實現了從堆中刪除任一結點的操作。

下面就以最大堆的構成為例,研究一下如何使用陣列實現堆。

最大堆

插入

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

  1. 完全二叉樹
  2. 任一結點的值是其左右子樹的最大值
  3. 用陣列實現

考慮下圖所示的堆。

假設現有元素60需要插入,為了維持完全二叉樹的特性,新插入的元素一定是放在結點44的右子樹;同時為了滿足任一結點的值要大於左右子樹的值這一特性,新插入的元素要和其父結點作比較,如果比父結點大,就要把父結點拉下來頂替當前結點的位置,自己則依次不斷向上尋找,找到比自己小的父結點就拉下來,直到沒有符合條件的值為止。這樣,到最後就完成了插入操作;總結一下:

  1. 新插入的結點新增到陣列最後
  2. 和其父結點比較大小,如果大於父結點,就用父結點替換當前位置,同時自己的位置上移。
  3. 直到父結點不再大於自己或者是位置已近到了陣列第一個位置,就找到屬於自己的位置了。

這裡為了方便,我們直接佔用了陣列下標為0的位置,在0的位置放置了一個null,這樣陣列中實際有效值的下標就和我們完全二叉樹中層序遍歷的實際序號對應了。這樣,完全二叉樹中,如果結點值為n,那麼其左子樹則為2n,右子樹為2n+1;換句話說,對於任一結點n,其父結點為n/2 取整即可。

  • 初始化堆
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介面。

  • 插入操作
/**
     * 堆的插入操作
     * @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());
    }
}複製程式碼

測試類就不在這裡佔篇幅了,有興趣的同學可以直接看原始碼.


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

相關文章