《算法撕裂者》01-手把手實現二叉堆

零、前言

(emm才發現這篇忘記發了=_=)上一篇我們學習了用堆實現優先隊列解決了經典的TopK問題,那這篇我就帶大家來手寫一個自己的堆吧。

一、二叉堆

我們首先來了解一下二叉堆。

二叉堆是一種特殊的堆,其實質是完全二叉樹(把元素順序排列成樹的形狀)。

二叉堆有兩種:最大堆和最小堆。最大堆是指父節點鍵值總是大於或等於任何一個子節點的鍵值。而最小堆恰恰相反,指的是父節點鍵值總是小於任何一個子節點的鍵值。如“圖1 最大堆”、“圖2 最小堆”所示:
圖1 最大堆
圖1 最大堆
圖2 最小堆圖2 最小堆

需要注意二叉堆與二叉樹的數據存儲結構不同,二叉樹是鏈式存儲,而二叉堆是線性存儲,知識邏輯結構用樹表示而是。 通常用數組來存儲二叉堆,並且可以得出父節點和子節點索引之間的關係。如下圖所示
在這裏插入圖片描述我這裏對應一部分功能給出一部分代碼,可能更好理解。最後會給出全部代碼。


    //    返回完全二叉樹的數組表示中,一個索引所表示的元素的父親節點的索引
    private int parent(int index) {
        if (index == 0)
            throw new IllegalArgumentException("index-0 does not have parent.");
        return (index - 1) / 2;
    }

    //    返回完全二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
    private int leftChild(int index) {
        return index * 2 + 1;
    }

    //    返回完全二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
    private int rightChild(int index) {
        return index * 2 + 2;
    }

二、二叉堆的操作

1、添加節點和上浮節點

在添加節點的時候,每添加的節點都是添加到二叉堆的最後一個位置,以最大堆爲例,如下圖所示:
在這裏插入圖片描述
添加節點10後發現其父節點小於新加的子節點,這不滿足最大堆的性質了,那可怎麼辦?所以有了接下來的上浮節點操作。

上浮節點 即判斷當前節點是否比父節點大,是的話則交換元素,繼續判斷。直到不大於父節點或者到達堆頂。下面將這個過程仔細說明並圖示。

添加新數據後(子節點10),我們讓子節點10與其父節點5作比較,父節點小於子節點,於是將子節點上浮與父節點轉換,如下圖:
在這裏插入圖片描述
接着再做同樣的操作,子節點10與父節點8作比較,同樣需要上浮,如圖;
在這裏插入圖片描述
再同樣做同樣操作,直至父節點大於子節點或者子節點已經上浮到根節點即止。如圖;
在這裏插入圖片描述
這裏先給出這部分的代碼。

    // 成員變量 data  注:這裏我使用的自己實現的動態數組!使用java的數組也可以
    private Array<E> data;

    //    向堆中添加元素
    public void add(E e) {
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    //    元素上浮
    private void siftUp(int k) {
        while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
            data.swap(k, parent(k));
            k = parent(k);
        }
    }

2、刪除節點和下沉節點

在刪除節點的時候,每次都是刪除二叉堆的根節點,最大堆即最大值,最小堆即最小值,如圖
在這裏插入圖片描述在刪除根節點之後,我們不可能讓根節點位置無主吧?於是將二叉堆最後一個數據填充至根節點,如圖;
在這裏插入圖片描述但是,我們又可以發現,填充上來的根節點比它的子節點小,這也不符合最大堆的性質呀!於是就有了下沉節點操作。

下沉節點 與上浮相反,判斷當前節點是否比子節點小,是的話則較大的子節點交換元素,繼續判斷。直到不小於子節點或者到達堆底。

直接上代碼:


    // 移除最大元素
    public E extractMax() {
        E e = findMax();
        // 交換最大值和最後一個
        data.swap(0, data.getSize() - 1);
        // 移除最後一個即最大值
        data.removeLast();
        // 下沉操作,維護順序
        siftDown(0);
        return e;
    }

    // 元素下沉
    private void siftDown(int k) {
        while (leftChild(k) < data.getSize()) {
            int j = leftChild(k);
            if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
                j = rightChild(k);
            }
            // data[j] 是 leftChild 和 rightChild 中的最大值
            if (data.get(k).compareTo(data.get(j)) >= 0)
                break;
            data.swap(j, k);
            k = j;
        }
    }

3、構建二叉堆

構建二叉堆,也就是把一個無序的完全二叉樹調整爲二叉堆,本質上就是讓所有非葉子節點依次下沉。

比如此時有一個無序的二叉樹,如圖;
在這裏插入圖片描述
首先從最後一個非葉子結點8開始,讓其與兩個子節點11、6作比較,然後做下沉節點操作,
在這裏插入圖片描述接着是節點2,做同樣的操作
在這裏插入圖片描述然後是節點5
在這裏插入圖片描述最後是節點12,因爲該節點都比其兩個子節點大,所以無需下沉。
最終,一顆無序的二叉樹就構建成了一個最大堆了。如圖
在這裏插入圖片描述
代碼:

    // 傳入數組的構造函數
    public MaxHeap(E[] arr){
        data = new Array<>(arr);
        for (int i = parent(data.getSize() - 1); i >= 0; i--) {
            siftDown(i);
        }
    }

4、代碼整合

/**
 * 動態數組 Array
 * 便於觀看我這隻留了需要的方法
 * @param <E>
 */
public class Array<E> {
    //成員變量
    private E[] data;
    private int size;

    //構造函數,傳入數組的容量capacity構造Array
    public Array(int capacity) {
        data = (E[])new Object[capacity]; //不能直接new 自定義類型的對象,間接new Object在轉型
        size = 0;
    }

    public Array(E[] arr){
        data = (E[])new Object[arr.length];
        for (int i = 0; i < arr.length; i++) {
            data[i] = arr[i];
        }
        size = arr.length;
    }
    
    //在所有元素後添加一個元素
    public void addLast(E e) {
        add(size, e);
    }

    public E removeLast(){
        return remove(size-1);
    }

    public void swap(int i,int j){
        if (i < 0 || i >= size || j < 0 || j >= size) {
            throw new IllegalArgumentException("Index is illegal!");
        }
        E t = data[i];
        data[i] = data[j];
        data[j] = t;
    }

}

/**
 * @description: 基於數組實現的最大堆
 * @author: Kevin
 * @createDate: 2020/2/21
 * @version: 1.0
 */
public class MaxHeap<E extends Comparable<E>> {
    // 成員變量 data  注:這裏我使用的自己實現的動態數組!使用java的數組也可以
    private Array<E> data;

    public MaxHeap(int capacity) {
        data = new Array<>(capacity);
    }

    public MaxHeap() {
        data = new Array<>();
    }

    // 傳入數組的構造函數
    public MaxHeap(E[] arr){
        data = new Array<>(arr);
        for (int i = parent(data.getSize() - 1); i >= 0; i--) {
            siftDown(i);
        }
    }

    public int getSize() {
        return data.getSize();
    }

    public boolean isEmpty() {
        return data.isEmpty();
    }

    //    返回完全二叉樹的數組表示中,一個索引所表示的元素的父親節點的索引
    private int parent(int index) {
        if (index == 0)
            throw new IllegalArgumentException("index-0 does not have parent.");
        return (index - 1) / 2;
    }

    //    返回完全二叉樹的數組表示中,一個索引所表示的元素的左孩子節點的索引
    private int leftChild(int index) {
        return index * 2 + 1;
    }

    //    返回完全二叉樹的數組表示中,一個索引所表示的元素的右孩子節點的索引
    private int rightChild(int index) {
        return index * 2 + 2;
    }

    //    向堆中添加元素
    public void add(E e) {
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    //    元素上浮
    private void siftUp(int k) {
        while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
            data.swap(k, parent(k));
            k = parent(k);
        }
    }

    // 找到最大元素
    public E findMax() {
        if (data.isEmpty())
            throw new IllegalArgumentException("Can not find from an empty heap!");
        return data.get(0);
    }

    // 移除最大元素
    public E extractMax() {
        E e = findMax();
        // 交換最大值和最後一個
        data.swap(0, data.getSize() - 1);
        // 移除最後一個即最大值
        data.removeLast();
        // 下沉操作,維護順序
        siftDown(0);
        return e;
    }

    // 元素下沉
    private void siftDown(int k) {
        while (leftChild(k) < data.getSize()) {
            int j = leftChild(k);
            if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
                j = rightChild(k);
            }
            // data[j] 是 leftChild 和 rightChild 中的最大值
            if (data.get(k).compareTo(data.get(j)) >= 0)
                break;
            data.swap(j, k);
            k = j;
        }
    }

}

三、基於二叉堆實現的優先隊列

至此,我們就可以基於二叉堆實現自己的優先隊列啦,既然java默認是基於最小堆實現的優先隊列,那麼我們就用最大堆來實現吧嘻嘻嘻

其實就是調用二叉堆的方法,上馬

/**
 * @description: 基於最大堆實現的優先隊列
 * @author: Kevin
 * @createDate: 2020/2/24
 * @version: 1.0
 */
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
    private MaxHeap<E> maxHeap;

    public PriorityQueue(){
        maxHeap = new MaxHeap<>();
    }

    @Override
    public int getSize() {
        return maxHeap.getSize();
    }

    @Override
    public boolean isEmpty() {
        return maxHeap.isEmpty();
    }

    @Override
    public void enqueue(E e) {
        maxHeap.add(e);
    }

    @Override
    public E dequeue() {
        return maxHeap.extractMax();
    }

    @Override
    public E getFront() {
        return maxHeap.findMax();
    }
}

四、總結

二叉堆的核心就在於上浮和下沉操作。

當添加元素是填添加到數組的最後一個位置,並判斷當前節點是否比父節點大,是的話則交換,繼續判斷。直到不大於父節點或者到達堆頂。

當刪除元素是刪除堆頂,此時先將堆頂與最後一個節點先交換,刪除最後一個節點即原堆頂。然後堆可能不滿足順序,從堆頂開始下沉操作。判斷當前節點是否比子節點小,是的話則較大的子節點交換元素,繼續判斷。直到不小於子節點或者到達堆底。

恭喜你又學到了知識,是不是還挺簡單的~

最後感謝閱讀,若有幫助點歌贊啦~

圖片引自:https://www.jianshu.com/p/6d3a12fe2d04

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