圖解二叉堆

二叉堆本質上其實就是一種完全二叉樹(不熟悉二叉樹的可以看前面的文章:圖解二叉樹),它分爲兩種類型:

  • 最大堆:堆中任何一個父節點的值都大於等於它左右子節點的值
  • 最小堆:顯然和最大堆相反,堆中任何一個父節點的值都小於等於它左右子節點的值

對於一個二叉堆的操作主要包含了兩個:插入節點和刪除節點;接下來會對這兩種操作進行具體的說明,說明的對象都是最小堆,如果理解了最小堆的操作,那麼最大堆的操作就易如反掌了。

插入節點 (最小堆)

插入一個節點主要的邏輯就是在二叉樹的最後節點上安置新的節點,然後比較此節點和父節點的大小。如果是小於父節點,那麼就不用操作,直接生成此二叉堆;如果是大於父節點,那麼將此節點和父節點位置互換(也就是上浮操作),將互換後的父節點作爲新的節點,再進行新節點和父節點的大小比較,直到新節點小於父節點或者新節點爲根節點,結束。文字的表現力依舊比較複雜,下面就看插入的流程圖吧,結合流程圖會更加貼切的理解插入過程。

  1. 在現有的二叉堆 ( 0, 3, 2, 7, 9, 5, 6, 10, 8 ) 中插入新的節點 1
  2. 新節點與父節點對比,新節點 1 小於 父節點 9,那麼就互換位置
  3. 互換後的節點 1 再與其父節點對比,1 小於其父節點 3,再次互換位置
  4. 再看此時的新節點 1,對於其現有父節點 0,它是大於其父節點的,所以終止上浮操作,生成最終的二叉堆 ( 0 1 2 7 3 5 6 10 8 9 ),如上圖所示。
    下面看具體的代碼實現:
    /**
     * 添加一個結點
     *
     * @param element 結點值
     */
    public void addNode(Integer element) {
        _data.add(element);
        up();
    }

    /**
     * 進行上浮操作
     */
    private void up() {
        int tempIndex = _data.size() - 1;
        int parentIndex = (tempIndex - 1) / 2;
        Integer temp = _data.get(tempIndex);

        while (tempIndex > 0) {
            int cmp = _data.get(parentIndex) - temp;
            if (cmp <= 0) {
                break;
            } else {
                _data.set(tempIndex, _data.get(parentIndex));
                tempIndex = parentIndex;
                parentIndex = (parentIndex - 1) / 2;
            }
        }
        _data.set(tempIndex, temp);
    }

刪除節點 (最小堆)

刪除節點的操作和邏輯要比添加節點稍微複雜那麼一丟丟,但是其本質還是一樣的,比較子節點和父節點的大小進行下沉操作。刪除某個節點,先將此節點移除,然後將最後一個節點暫時安放在刪除節點的位置;開始比較最後一個節點和其左右子節點的大小,如果是比兩個子節點都小,那麼終止下沉操作,生成新的二叉堆,如果是比其中任何一個子節點大,那麼就將此節點和其子節點中最小的那個互換位置,依次循環,直到沒有子節點或者比子節點小,終止下沉操作,生成最終的新二叉堆。下面請看圖解:

  1. 在原二叉堆 ( 0 1 2 7 3 5 6 10 8 9 ) 中刪除其根節點 0,第一步就是將最後節點 9 暫時移到根節點處
  2. 第二步對比 9 這個節點和其子節點中最小的大小,最小節點爲 1,而且 9 比其子節點 1 要大,那麼就將其互換位置,得到如下圖所示:
  3. 第三步繼續對比此時的 9 節點和其子節點的大小,此時它的子節點中最小的是 3 ,而 9 節點又比 3 要大,那麼繼續互換位置,如下圖:
  4. 第四步準備繼續對比,發現此時的 9 節點沒有子節點了,那麼就終止對比,生成新的二叉堆 ( 1 3 2 7 9 5 6 10 8 ):代碼實現起來如下:
/**
     * 刪除一個結點,同時進行下沉操作
     *
     * @param element 刪除的結點
     */
    public void deleteNode(Integer element) {
        if (_data.isEmpty()) {
            return;
        }
        int index = _data.indexOf(element);
        if (index == -1) {
            return;
        }
        if (index == _data.size() - 1) {
            _data.remove(_data.size() - 1);
            return;
        }
        int size = _data.size();
        _data.set(index, _data.get(size - 1));
        _data.remove(size - 1);
        if (size > 1) {
            down(index, _data.size() - 1);
        }
    }

    /**
     * 下沉
     *
     * @param start 下沉起始座標
     * @param end   下沉終點座標
     */
    private void down(int start, int end) {
        int left = start * 2 + 1;
        Integer temp = _data.get(start);
        while (left <= end) {
            boolean max = true;
            // 防止index越界
            if (left + 1 <= end) {
                // 對比左右子節點,如果右子節點比左小,那麼left取右子節點的下標
                max = (_data.get(left) - _data.get(left + 1)) < 0;
            }
            if (!max && left <= end) {
                left++;
            }
            // 如果子節點小於父節點,那麼交換位置
            // 並且繼續檢查子節點和其子節點的大小關係
            max = (temp - _data.get(left)) > 0;
            if (!max) break;
            else {
                _data.set(start, _data.get(left));
                start = left;
                left = left * 2 + 1;
            }
        }
        _data.set(start, temp);
    }

二叉堆在理解二叉樹的知識之後,上手理解還是比較輕鬆的,而且二叉堆的應用很廣,比如我們使用的堆排序,就是完美的使用了二叉堆中刪除節點的操作,可以試想一下,對於一個最小堆來說,依次刪除根節點,拿到順序不就是從最小值開始的麼。而且我們還可以取一個數組或者列表中前K個最小值。所以二叉堆的知識還是有必要熟練掌握的!
下面我把整體代碼貼出來,以便大家在熟悉過程中可以跟着代碼結果來對比。

import java.util.ArrayList;
import java.util.List;

class BinaryHeap {

    private List<Integer> _data;

    public BinaryHeap() {
        _data = new ArrayList<>();
    }

    /**
     * 添加一個結點
     *
     * @param element 結點值
     */
    public void addNode(Integer element) {
        _data.add(element);
        up();
    }

    /**
     * 進行上浮操作
     */
    private void up() {
        int tempIndex = _data.size() - 1;
        int parentIndex = (tempIndex - 1) / 2;
        Integer temp = _data.get(tempIndex);

        while (tempIndex > 0) {
            int cmp = _data.get(parentIndex) - temp;
            if (cmp <= 0) {
                break;
            } else {
                _data.set(tempIndex, _data.get(parentIndex));
                tempIndex = parentIndex;
                parentIndex = (parentIndex - 1) / 2;
            }
        }
        _data.set(tempIndex, temp);
    }

    /**
     * 刪除一個結點,同時進行下沉操作
     *
     * @param element 刪除的結點
     */
    public void deleteNode(Integer element) {
        if (_data.isEmpty()) {
            return;
        }
        int index = _data.indexOf(element);
        if (index == -1) {
            return;
        }
        if (index == _data.size() - 1) {
            _data.remove(_data.size() - 1);
            return;
        }
        int size = _data.size();
        _data.set(index, _data.get(size - 1));
        _data.remove(size - 1);
        if (size > 1) {
            down(index, _data.size() - 1);
        }
    }

    /**
     * 下沉
     *
     * @param start 下沉起始座標
     * @param end   下沉終點座標
     */
    private void down(int start, int end) {
        int left = start * 2 + 1;
        Integer temp = _data.get(start);
        while (left <= end) {
            boolean max = true;
            // 防止index越界
            if (left + 1 <= end) {
                // 對比左右子節點,如果右子節點比左小,那麼left取右子節點的下標
                max = (_data.get(left) - _data.get(left + 1)) < 0;
            }
            if (!max && left <= end) {
                left++;
            }
            // 如果子節點小於父節點,那麼交換位置
            // 並且繼續檢查子節點和其子節點的大小關係
            max = (temp - _data.get(left)) > 0;
            if (!max) break;
            else {
                _data.set(start, _data.get(left));
                start = left;
                left = left * 2 + 1;
            }
        }
        _data.set(start, temp);
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();
        _data.forEach(element -> result.append(element).append(" "));
        return result.toString();
    }

    public static void main(String[] args) {
        Integer[] data = {0, 3, 2, 7, 9, 5, 6, 10, 8};
        BinaryHeap binaryHeap = new BinaryHeap();
        for (Integer element : data) {
            binaryHeap.addNode(element);
        }
        binaryHeap.addNode(1);
        System.out.println(binaryHeap.toString());
        binaryHeap.deleteNode(0);
        System.out.println(binaryHeap.toString());
    }
}

如果本文章你發現有不正確或者不足之處,歡迎你在下方留言或者掃描下方的二維碼留言也可!

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