TypeScript實現二叉堆

前言

二叉堆是計算機科學中一種很是著名的數據結構,因爲它能高效、快速地找出最大值和最小值所以常被用於優先隊列和堆排序算法。javascript

本文將詳解二叉堆並用TypeScript將其實現,歡迎各位感興趣的開發者閱讀本文。java

寫在前面

本文重點講解堆如何實現,對堆這種數據結構不瞭解的開發者請移步個人另外一篇文章:數據結構:堆git

實現思路

二叉堆是一種特殊的二叉樹,二叉堆也叫堆,它有如下兩個特性:github

  • 它是一顆徹底二叉樹
  • 二叉堆不是最小堆就是最大堆

徹底二叉樹

一顆徹底二叉樹,它的每一層都有左側和右側子節點(除過最後一層的葉節點),而且最後一層的葉節點儘量都是左側子節點。算法

下圖描述了一顆徹底二叉樹: typescript

ff4a1d04c3077e98fcc7aefe0b074660

最小堆和最大堆

  • 最小堆:全部的節點都小於等於它的子節點
  • 最大堆:全部的節點都大於等於它的子節點

下圖描述了最大堆和最小堆 api

1e11627d68e032295c9a2c140aa1ee26

實現二叉堆

二叉堆有兩種表現方式:數組

  • 像二叉樹同樣用節點表示
  • 使用數組表示,經過索引值檢索父節點、左側、右側節點的值

下圖描述了兩種不一樣的表示方式 數據結構

31e8c437e0d3b1d837693c1397ef9784

操做堆節點

咱們使用數組來表示二叉堆,對於給定位置(index)的節點,咱們能夠對其進行以下操做:函數

  • 獲取給定節點的左側子節點位置:2 * index + 1
  • 獲取給定節點的右側子節點位置:2 * index + 2
  • 獲取給定節點的父節點位置:(index - 1) / 2
向堆中插入數據

向堆中插入數據(insert)是指將數據插入堆的底部葉節點再執行上移(siftUp), 表示咱們將要把這個數據和它的父節點進行交換,直到父節點小於這個插入的值。

  • insert方法接收一個參數:要插入的數據
  • 須要對插入的數據進行非空判斷,若是爲null則返回false
  • 數據不爲空時,往數組(heap)的末尾追加要插入的數據
  • 插入完成後,執行siftUp操做,將數據移動至合適的位置
  • 上移完成後,則成功的向堆中插入了一條數據,返回true

上移操做的實現以下:

  • siftUp方法接收一個參數:插入數據的索引位置(index)
  • 獲取當前要插入數據的父節點位置(parent)
  • index大於0且heap[parent] > heap[index],交換parent和index位置的節點
  • 更新index和parent的值,繼續進行節點交換直至heap[parent] < heap[index]

交換的實現以下:

  • swap接收三個參數:要操做的數組,交換的元素位置,被交換的元素位置
  • 聲明一個臨時變量temp,賦值交換的元素
  • 交換的元素賦值爲被交換的元素
  • 被交換的元素賦值爲temp

接下來咱們用一個例子來描述上述插入過程,以下圖所示爲一個最小堆,咱們要插入一個新的節點2。

7505a622b6746b8299214e1a22be3bc8

  • 找到它的父節點12,比較12與2的大小,12 > 2,進行位置互換
    598e289fbc5e210c6c46f96ae329951c
  • 此時2的父節點是5,5 > 2,進行位置交換
    250af609d5f7e84703f5d891f3b73332
  • 2此時2的父節點是1,1 < 2,插入完成
    3cc4206fdc04511fc8ae99706de753c6
尋找堆中的最大值或最小值
  • 在最小堆中數組的0號元素就是堆的最小值
  • 在最大堆中數組的0號元素就是堆的最大值
導出堆中的最小值或最大值

移除最小值(最小堆)或最大值(最大堆)表示移除數組中的第一個元素(堆的根節點)。

在移除後,咱們須要將堆的最後一個元素移動至根部並執行下移(siftDown)函數,表示咱們將交換元素直到堆的結構正常。

  • extract函數不接收參數
  • 若是堆爲空則返回undefined
  • 若是堆的長度爲1,直接返回堆頂元素
  • 不然,聲明一個變量保存堆頂元素
  • 執行下移函數調整堆結構
  • 返回剛纔保存堆堆頂元素

下移操做的實現:

  • siftDown函數接收一個參數:須要調整的元素位置(index)
  • 聲明一個變量(element)保存index
  • 獲取index的左子節點(left)、右子節點(right)、堆的大小(size)
  • 若是heap[element] > heap[left],則更新element的值爲left
  • 若是heap[element] > heap[right],則更新element的值爲right
  • 若是index !== element,則交換index和element位置的元素,繼續執行siftDown函數

接下來,咱們經過一個例子來說解上述執行過程,下圖描述了一個最小堆

dfce37620fbc50fc2d41445bc3eb10b5

  • 咱們導出堆頂節點1
    692027da02c2a7cd3fbde7315418ce6d
  • 此時,咱們須要把堆的最後一個節點放到堆頂
    e98717c0a90b5c28c1b70082e8327b24
  • 此時,進行下移操做,比較12和其左子節點2的大小,12 > 2,交換節點位置
    9bfb092fb1d71552cf7d168b1353e51d
  • 繼續進行下移操做,比較12和其左子節點5的大小,12 > 5,交換節點位置
    ce76ce44d5cca67ee205e18e3cee4e34
  • 此時index === element,下移操做完成,堆節點導出完成
    f2754d854ab372d6ef5a1bcf335e135b
實現最大堆

上述操做咱們實現了一個最小堆,最大堆與最小堆的別就在於節點的比較,所以咱們只須要繼承最小堆,重寫比對函數,將原來的a與b比較,改成b與a比較便可。

實現代碼

上面咱們講解了堆的概念,分析了的實現思路,接下來咱們將上述實現思路轉化爲代碼

  • 新建Heap.ts文件
  • 聲明MinHeap類,聲明堆、比對函數、初始化堆
export class MinHeap<T> {
    // 用數組來描述一個堆
    protected heap: T[];

    constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {
        this.heap = [];
    }
}
複製代碼
  • 實現獲取左、右、父節點函數
// 獲取左子節點的位置
    protected getLeftIndex(index: number): number {
        return 2 * index + 1;
    }

    // 獲取右子節點的位置
    protected getRightIndex(index: number): number {
        return 2 * index + 2;
    }

    // 獲取父節點的位置
    protected getParentIndex(index: number): number | undefined {
        if (index === 0) {
            return undefined;
        }
        return Math.floor((index - 1) / 2);
    }
複製代碼
  • 實現插入函數
insert(value: T): boolean {
        if (value != null) {
            // 向堆的葉結點添加元素,即數組的尾部
            this.heap.push(value);
            // 進行上移操做,即上移節點至合適的位置
            this.siftUp(this.heap.length - 1);
            return true;
        }
        return false;
    }

    // 實現上移函數
    protected siftUp(index: number): void {
        // 獲取父節點位置
        let parent = <number>this.getParentIndex(index);
        // 插入的位置必須大於0,且它的父節點大於其自己就執行循環裏的操做
        while (index > 0 && this.compareFn(this.heap[parent], this.heap[index]) === Compare.BIGGER_THAN) {
            // 交換元素的位置
            this.swap(this.heap, parent, index);
            // 修改當前插入值的位置爲它的父節點,從新獲取父節點的位置,即重複這個過程直到堆的根節點也通過了交換
            index = parent;
            parent = <number>this.getParentIndex(index);
        }
    }

    // 實現交換數組元素位置函數
    protected swap(array: T[], exchangeElement: number, exchangedElement: number): void {
        // 用一個臨時變量保存交換元素
        const temp = array[exchangeElement];
        // 將被交換元素賦值給交換元素
        array[exchangeElement] = array[exchangedElement];
        // 將第一步保存的臨時變量賦值給被交換元素
        array[exchangedElement] = temp;
    }    
複製代碼
  • 實現尋找堆的最小值函數
findMinimum(): T | undefined {
        // 返回數組的最小元素
        return this.isEmpty() ? undefined : this.heap[0];
    }

    // 判斷堆是否爲空
    isEmpty(): boolean {
        return this.size() === 0;
    }
複製代碼
  • 實現導出堆的最小值函數
extract(): T | undefined {
        if (this.isEmpty()) {
            return undefined;
        }

        if (this.size() === 1) {
            // 返回數組的第一個元素
            return this.heap.shift();
        }

        const removedValue = this.heap.shift();
        // 執行下移操做
        this.siftDown(0);
        return removedValue;
    }

    // 下移操做
    protected siftDown(index: number): void {
        // 保存當前插入值的位置
        let element = index;
        // 獲取其左、右子節點的位置
        const left = this.getLeftIndex(index);
        const right = this.getRightIndex(index);
        const size = this.size();
        // 元素有效,且當前元素大於其左子節點
        if (left < size && this.compareFn(this.heap[element], this.heap[left]) === Compare.BIGGER_THAN) {
            element = left;
        }

        // 元素有效,當前元素大於其右子節點
        if (right < size && this.compareFn(this.heap[element], this.heap[right]) === Compare.BIGGER_THAN) {
            element = right;
        }

        // 找到最小子節點的位置,校驗它的值是否和element相同
        if (index !== element) {
            // 若是不相同將它和最小的element進行交換
            this.swap(this.heap, index, element);
            // 遞歸執行
            this.siftDown(element);
        }
    }
複製代碼

完整代碼地址:Heap.ts

堆排序

堆的一種應用就是堆排序,此處不講解堆排序的實現思路,對堆排序不瞭解的開發者請移步個人另外一篇文章: 排序算法:堆排序的理解與實現

  • 實現堆排序函數
heapSort(array: T[]): void {
        // 構建堆
        this.buildHeap(array);
        // 從堆的末尾開始遍歷,將遍歷到的元素與0好元素進行交換,而後執行下移操做
        for (let i = array.length - 1; i >= 0; i--) {
            this.swap(array, i, 0);
            this.heapify(array, i, 0);
        }
    }

    // 構建堆
    private buildHeap(array: T[]) {
        // 獲取最後一個節點的位置
        const last = array.length - 1;
        const lastParent = <number>this.getParentIndex(last);
        // 從最後一個節點的父節點開始進行heapify操做
        for (let i = lastParent; i >= 0; i--) {
            this.heapify(array, array.length, i);
        }
    }

    // 交換節點
    private heapify(array: T[], size: number, index: number) {
        // 遞歸基線條件
        if (index >= size) {
            return false;
        }

        // 找到當前要操做節點的左、右子樹
        const left = this.getLeftIndex(index);
        const right = this.getRightIndex(index);
        // 保存當前要操做節點的位置
        let element = index;

        // 若是當前要操做節點的左子節點大於其父節點,更新element的值
        if (left < size && this.compareFn(array[left], array[element]) === Compare.BIGGER_THAN) {
            element = left;
        }

        // 若是當前要操做節點的右子節點大於其父節點,更新element的值
        if (right < size && this.compareFn(array[right], array[element]) === Compare.BIGGER_THAN) {
            element = right;
        }

        // element的位置不等於當前要操做節點,交換元素位置,遞歸執行
        if (element !== index) {
            this.swap(array, element, index);
            this.heapify(array, size, element);
        }
    }
複製代碼

編寫測試代碼

接下來咱們測試下上述代碼是否正常執行

import { MinHeap, MaxHeap } from "./lib/Heap.ts";

const minHeap = new MinHeap();
minHeap.insert(13);
minHeap.insert(10);
minHeap.insert(5);
minHeap.insert(7);
minHeap.insert(4);
minHeap.insert(17);
console.log("堆(min)的全部元素", minHeap.getIsArray());
console.log("堆(min)的最小值", minHeap.findMinimum());
console.log(minHeap.extract());
console.log(minHeap.getIsArray());
console.log("---------------------------------------");
const maxHeap = new MaxHeap();
maxHeap.insert(13);
maxHeap.insert(10);
maxHeap.insert(5);
maxHeap.insert(7);
maxHeap.insert(4);
maxHeap.insert(17);
console.log("堆(max)的全部元素", maxHeap.getIsArray());
console.log(maxHeap.extract());
console.log("堆(max)的最大值", maxHeap.findMinimum());
console.log("---------------------------------------");
const arrayTest = [12, 15, 17, 18, 4, 5, 1, 7, 19, 20];
minHeap.heapSort(arrayTest);
console.log(arrayTest);
複製代碼

89f561e9c0e3bd20b6a2c198d79a582f

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注😊
  • 本文首發於掘金,未經許可禁止轉載💌
相關文章
相關標籤/搜索