資料結構-堆

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

Heap(堆)

Heap學前知識

堆的概念:
N個元素序列[k1, k2, k3, k4, k5, k6...kn]當且僅當滿足以下關係時才會被稱為堆。

當資料下標為1時: ki <= k2i, ki <= k2i+1 或者 ki >= k2i, ki >= k2i + 1
當資料下標為0時: ki <= k2i + 1, ki <= k2i + 2 或者 ki >= k2i + 1, ki >= k2i + 2
複製程式碼

堆(heap)的實現通常是通過構造二叉堆, 因為應用較為普遍, 當不加限定時, 堆通常指的就是二叉堆。

二叉堆:

  • 二叉堆是一棵完全二叉樹(參考圖1-1)
  • 堆中的節點值總是不大於其父親節點的值, 這種我們一般稱為最大堆。反之亦然我們稱為最小堆。(參考圖1-2)
  • 利用陣列實現二叉堆(參考圖1-3)
    • 使用下標0的公式:
        parent(i) = i / 2
        left child (i) = 2 * i
        right child (i) = 2 * i + 1
      複製程式碼
    • 使用下標1的公式:
        parent(i) = (i - 1) / 2
        left child (i) = 2 * i + 1
        right child (i) = 2 * i + 2
      複製程式碼

圖1-1

avatar


圖1-2

avatar


圖1-3

avatar


最大堆的設計實現

初步結構

由於堆中的元素是需要進行比較的, 所以插入進來的元素都是需要帶有可比較性的。這裡我們繼承Comparable即可。 這裡我們用的是java自帶的動態陣列, 這樣避免空間不充足問題。

為了方便查詢元素的父節點以及左右孩子節點, 我們將其封裝成方法, 這樣無論是從陣列下標0或者下標1開始對於我們來說 都是不關的, 我們只關心返回正確的節點索引位置資料。


public class MaxHeap<E extends Comparable<E>> {

    private ArrayList<E> data;

    public MaxHeap(int capacity) {
        this.data = new ArrayList<>(capacity);
    }

    public MaxHeap() {
        this.data = new ArrayList<>();
    }

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

    public boolean isEmpty() {
        return data.size() == 0;
    }

    // 返回父元素在二叉堆中陣列的索引位置
    private int parent(int index) {
        if (index == 0)
            throw new IllegalArgumentException("該索引沒有父節點");
        return (index - 1) / 2 ;
    }

    // 返回左孩子索引
    private int leftChild(int index) {
        return index * 2 + 1;
    }

    // 返回孩子索引
    private int rightChild(int index) {
        return index * 2 + 2;
    }

}
複製程式碼
新增元素及上濾

現在我們需要向陣列中新增元素, 但是新增進入的元素是否滿足堆的特性呢? 如果不滿足我們又要如何去處理呢?

當我們向陣列中新增一個元素且不滿足堆的特性時候, 我們需要進行一個上濾的過程(有些稱為上浮), 用來達到並滿足堆的特性。

如下圖所示, 向陣列中新增一個元素並且進行上濾的過程:

avatar

通過上面的圖例過程, 我們清楚在插入的時候需要一直和自己父節點進行比較, 直到滿足 堆的特性才算插入成功。

// 新增元素
public void add(E e) {
    data.add(e);
    // 將新加入的索引進行上濾, 新增是從最後新增的所以取最後元素索引位置
    siftUp(data.size() - 1);
}

// 上濾過程
private void siftUp(int index) {
    // 如果當前index為0表示為根節點, 根節點是沒有父元素的所以沒法比較, 並且父節點是小於子節點的
    while (index > 0 && data.get(parent(index)).compareTo(data.get(index)) < 0) {
        // 如果子節點大於父節點進行交換
        swap(parent(index), index);
        // 繼續判斷, 是否還大於祖先節點, 直到滿足堆的特性
        index = parent(index);
    }
}

// 位置交換
private void swap(int p, int c) {
    E t = data.get(p);
    data.set(p, data.get(c));
    data.set(c, t);
}
複製程式碼
取出元素及下濾

由於堆是優先佇列的結構, 所以只能從堆頂刪除元素。移除堆頂元素之後, 用堆的最後一個節點填補取走的堆頂元素, 並將堆的實際元素個數減1。但用最後一個元素取代堆頂元素可能會破壞堆的特性, 因此需要將堆自頂向下進行調整(這個過程一般稱為下浮或者下濾)使其滿足最大堆或者最小堆。

下圖是取出元素並進行下濾流程圖:

avatar


// 查詢出最大元素
public E findMax() {
    if (data.size() == 0)
        throw new IllegalArgumentException("當前陣列為空");
    return data.get(0);
}

// 取出堆中最大元素
public E extractMax() {
    // 1. 找到最大值
    E ret = findMax();

    // 2. 最後一個元素頂替最大值
    swap(0, data.size() - 1);

    // 3. 刪除最後節點值
    data.remove(data.size() - 1);

    // 4. 下濾(下浮)過程
    siftDown(0);

    return ret ;
}

// 下濾節點
private void siftDown(int index) {

    // 如果下濾到葉子節點, 在去獲取當前索引位置左孩子肯定超出整個陣列大小
    while (leftChild(index) < data.size()) {


        // 1. 獲取到該索引的左右孩子
        int k = leftChild(index);

        // 需要判斷是否存在右孩子
        // k + 1的話相當左孩子索引位置+1得到了右孩子, 如果不大於陣列長度則包含右孩子
        if (k + 1 < data.size()) {

            // 獲取左右孩子中最大的元素節點
            // 這裡的判斷是更新索引位置資料, 如果左孩子大於右孩子則不需要更新索引, 否則更新為右孩子的索引
            if (data.get(k).compareTo(data.get(k + 1)) < 0) {
//                    k = rightChild(index);
                ++ k; // ++k 等價rightChild(k)
            }
        }

        if (data.get(index).compareTo(data.get(k)) >= 0)
            break;

        // 進行交換資料
        swap(index, k);
        // 將下濾後的索引繼續進行判斷
        index = k;
    }
}
複製程式碼
Replace和Heapify處理
  • Replace 定義: 取出堆中最大的元素, 然後放入一個新的元素。

    實現原理:

    1. 可以直接將堆頂元素替換成新的元素, 然後進行下濾(下沉)操作。
  • Heapify

定義: 將任意陣列轉換成堆。

實現原理:
從最後一個非葉子節點開始計算。如圖1-4,我們這個棵完全二叉樹有5個葉子節點。
相應的倒數第一個非葉子節點就是元素22所在的節點。我們從這個節點開始倒著從後向前進行下濾操作。

如何定位最後一個非葉子節點索引位置呢?
取出陣列最後一個索引位置, 更具最後一個索引位置計算獲取到父節點索引位置。

圖1-4

avatar



public E replace(E e) {
    // 1. 找到最大元素
    E ret = findMax();

    // 2. 新插入的值替換堆頂元素
    data.set(0, e);

    // 3. 進行下沉操作
    siftDown(0);

    return ret;
}


/**
  Heapify操作, 寫成一個建構函式.
*/
public MaxHeap(ArrayList<E> data) {

   this.data = data;
   // 縮寫, 直接獲取到最後一個非葉子節點索引, 進行遞減。
   for (int i = parent(data.size() - 1); i >= 0; i--) {
       siftDown(i);
   }

   // 1. 獲取到最後一個非葉子節點的元素索引位置
//        int p = parent(this.data.size() - 1);
   // 2. 對p從後往前執行, 依次遞減進行下沉操作
//        while (p >= 0) {
//            // 2. 進行下沉
//            siftDown(p);
//            p--;
//        }
}
複製程式碼

avatar

相關文章