数据结构 101:如何在 JavaScript 中构建最小堆和最大堆

124 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情

什么是堆?

堆是一种高级的基于树的数据结构, 主要用于排序和实现优先级[队列]它们是具有以下特征的完全二叉树:

  • 除了叶节点(没有子节点的节点称为叶)之外,每个级别都被填充。
  • 每个节点最多有 2 个孩子。
  • 所有节点都尽可能靠左。这意味着每个孩子都在他父母的左边。

堆

堆使用完全二叉树来避免数组中出现空洞。完全二叉树是每个结点至多有两个孩子的树,所有层的结点都是满的,除了叶子结点可以为空。堆是基于堆属性构建的,堆属性将父节点键与其子节点键进行比较。

重要的是要注意堆并不总是排序的。它们遵循的关键条件是最大或最小元素放置在根节点(顶部)上,具体取决于它是最大堆还是最小堆。堆数据结构与堆内存不同。

堆的优点和缺点

优点:

  • 垃圾收集在堆内存上运行以释放对象使用的内存。
  • 堆是灵活的,因为可以按任何顺序分配或删除内存。
  • 变量可以全局访问。
  • 它有助于找到最小和最大的数字。

缺点:

  • 与栈相比,堆需要更多的时间来执行。
  • 内存管理在堆内存中更为复杂,因为它是全局使用的。
  • 堆通常需要更多的时间来计算。

堆数据结构的应用

堆对于查找数组中的最小或最大元素非常有效,并且在顺序统计和选择算法中很有用。从堆中获取最小值/最大值的时间复杂度为O(1) ,(恒定时间复杂度)。

优先级队列是基于堆结构设计的。有效地插入 ( ) 和删除 ( ) 优先级队列中的每个元素需要O(log(n))时间。 insert()``delete()

堆实现的优先级队列用于流行的算法,例如:

  • 普里姆算法
  • Dijkstra 算法
  • 堆排序算法

堆中的基本操作

以下是您在实现堆数据结构时可能使用的基本操作:

  • heapify 重新排列堆中的元素以保持堆属性。
  • insert 将一个项目添加到堆中,同时保持其堆属性。
  • delete 删除堆中的项目。
  • extract 返回一个项目的值,然后从堆中删除它。
  • isEmpty:  boolean,如果 boolean 为空则返回 true,如果有节点则返回 false。
  • size: 返回堆的大小。
  • getMax() 返回堆中的最大值

如何构建最大堆

最大堆中的元素遵循最大堆属性。这意味着父节点的键总是大于两个子节点的键。要构建最大堆:

  • 在堆的开头(根)创建一个新节点。
  • 给它赋值。
  • 将子节点的值与父节点进行比较。
  • 如果父节点的值小于任一子节点(向左或向右)的值,则交换节点。
  • 重复直到最大元素位于根父节点(然后你可以说堆属性成立)。

将新元素插入堆时也可以遵循这些步骤。这里的关键是无论在最大堆上执行什么操作,堆属性都必须保持。

移除/删除最大堆中的根节点:

  • 删除根节点。
  • 将最后一层的最后一个子节点移动到根。
  • 将父节点与其子节点进行比较。
  • 如果父节点的值小于子节点,则交换它们,并重复直到满足堆属性。

让我们看一下这在代码中是什么样子的。在下一节中,我们将使用[JavaScript] 实现最大堆。

在 JavaScript 中实现最大堆

在我们开始构建最大堆之前,先看看我们将要实现的一些方法以及它们的作用:

  • _percolateUp() 将堆属性从子节点恢复到根节点。
  • _maxHeapify() 将堆属性从特定节点向下恢复到叶节点。
  • insert() 将给定值附加到堆数组并根据元素的堆属性重新排列元素。在每次插入时,堆均匀增长,大小增加一。
  • getMax() 返回堆(根节点)中的最大值,不修改堆。注意这里的时间复杂度是常数时间O(1)
  • removeMax() 返回并删除堆中的最大值(想想pop())。该函数的时间复杂度为O(log(n))

如果堆大小大于一,它将最大值存储到一个变量中,将该值与最后一个叶子交换,然后从堆中删除最大值。

如果堆只有一个元素,则删除并返回该元素的值,最后一个条件是如果堆为空,则返回null。

__percolateUp()方法在每个父节点上递归[调用] ,直到到达根节点。对于要定位在 max-heap 属性之后的每个节点,我们在该数组的每个索引处调用该__maxHeapify()方法,从堆的底部开始。

class maxHeap {
    constructor() {
        this.heap = [];
        this.elements = 0;
    };

    insert(val) {
        if (this.elements >= this.heap.length) {
            this.elements = this.elements + 1;
            this.heap.push(val);
            this.__percolateUp(this.heap.length - 1);
        }
        else {
            this.heap[this.elements] = val;
            this.elements = this.elements + 1;
            this.__percolateUp(this.elements - 1);
        }
    };

    getMax() {
        if (this.elements !== 0)
            return this.heap[0];
        return null;
    };

    removeMax() {
        let max = this.heap[0];
        if (this.elements > 1) {
            this.heap[0] = this.heap[this.elements - 1];
            this.elements = this.elements - 1;
            this.__maxHeapify(0);
            return max
        } else if (this.elements === 1) {
            this.elements = this.elements - 1;
            return max;
        } else {
            return null;
        }
    };

    __percolateUp(index) {
        const parent = Math.floor((index - 1) / 2);
        if (index <= 0)
            return
        else if (this.heap[parent] < this.heap[index]) {
            let tmp = this.heap[parent];
            this.heap[parent] = this.heap[index];
            this.heap[index] = tmp;
            this.__percolateUp(parent);
        }
    };

    __maxHeapify(index) {
        let left = (index * 2) + 1;
        let right = (index * 2) + 2;
        let largest = index;
        if ((this.elements > left) && (this.heap[largest] < this.heap[left])) {
            largest = left
        }
        else if ((this.elements > right) && (this.heap[largest] < this.heap[right]))
            largest = right
        else if (largest !== index) {
            const tmp = this.heap[largest];
            this.heap[largest] = this.heap[index];
            this.heap[index] = tmp;
            this.__maxHeapify(largest);
        }
    };

    buildHeap(arr) {
        this.heap = arr;
        this.elements = this.heap.length;
        for (let i = this.heap.length - 1; i >= 0; i--) {
            this.__maxHeapify(i);
        }

    };
};
let heap = new maxHeap();

如何构建最小堆

直观上,我们可以说最小堆中的元素遵循最小堆属性,因为这与最大堆相反。父节点的键总是小于两个子节点的键。要构建最小堆:

  • 在堆的末尾(最后一层)创建一个新的子节点。
  • 将新键添加到该节点(将其附加到数组)。
  • 向上移动子节点,直到到达根节点并且堆属性得到满足。

删除/删除最小堆中的根节点:

  • 删除根节点。
  • 将最后一个孩子的钥匙移到根。
  • 将父节点与其子节点进行比较。
  • 如果父节点的值大于子节点,则交换它们,并重复直到满足堆属性。

在 JavaScript 中实现最小堆

在我们开始构建最小堆之前,请注意它的实现类似于最大堆。minHeapify()恢复堆属性。getMin()返回堆(根节点)中的最小值而不修改堆。并removeMin()删除最小值并返回它。

class minHeap {
    constructor() {
        this.heap = []
        this.elements = 0;
    };

    insert(val) {
        if (this.elements >== this.heap.length) {
            this.elements = this.elements + 1
            this.heap.push(val);
            this.__percolateUp(this.heap.length - 1);
        }
        else {
            this.heap[this.elements] = val;
            this.elements = this.elements + 1;
            this.__percolateUp(this.elements - 1);
        }
    };

    getMin() {
        if (this.heap.length !== 0)
            return this.heap[0];
        return null;
    }

    removeMin() {
        const min = this.heap[0];
        if (this.elements > 1) {            
            this.heap[0] = this.heap[this.elements - 1];
            this.elements = this.elements - 1;
            this.__minHeapify(0);
            return min;
        } else if (this.elements == 1) {
            this.elements = this.elements - 1;
            return min;
        } else {
            return null;
        }
    };

    __percolateUp(index) {
        let parent = Math.floor((index - 1) / 2);
        if (index <= 0)
            return
        else if (this.heap[parent] > this.heap[index]) {
            let tmp = this.heap[parent];
            this.heap[parent] = this.heap[index];
            this.heap[index] = tmp;
            this.__percolateUp(parent);
        }
    };

    __minHeapify(index) {
        let left = (index * 2) + 1;
        let right = (index * 2) + 2;
        let smallest = index;
        if ((this.elements > left) && (this.heap[smallest] > this.heap[left])) {
            smallest = left;
        }
        if ((this.elements > right) && (this.heap[smallest] > this.heap[right]))
            smallest = right;
        if (smallest !== index) {
            let tmp = this.heap[smallest];
            this.heap[smallest] = this.heap[index];
            this.heap[index] = tmp;
            this.__minHeapify(smallest);
        }
    }

    buildHeap(arr) {
        this.heap = arr;
        this.elements = this.heap.length;
        for (let i = this.heap.length - 1; i >= 0; i--) {
            this.__minHeapify(i)
        }
    }
};

let heap = new minHeap();
heap.insert(12);
heap.insert(10);
heap.insert(-10);
heap.insert(100);

console.log(heap.getMin()); //you should get -10

let newheap = new minHeap();
let arr = [12, 6, 8, 3, 16, 4, 27];
newheap.buildHeap(arr) //builds this new heap with elements from the array
console.log(newheap.getMin()) //this logs 3

newheap.removeMin();

console.log(newheap.getMin())

堆挑战:将最大堆转换为最小堆

让我们通过动手挑战来进一步学习。我们的目标是将最大堆转换为最小堆。跟随我们的代码解决方案,看看它是如何完成的。

问题陈述: 实现一个函数convertMax(maxHeap),将二进制最大堆转换为二进制最小堆,其中maxHeap是格式中的数组maxHeap(即父项大于其子项)。您的输出应该是一个转换后的数组。

示例输入:

maxHeap = [9,4,7,1,-2,6,5]

示例输出:

result = [-2,1,5,9,4,6,7]

最小到最大堆

问题:

function convertMax(maxHeap) {
    return maxHeap
}

解决方案:

function minHeapify(heap, index) {
    var left = index * 2;
    var right = (index * 2) + 1;
    var smallest = index;

    if ((heap.length > left) && (heap[smallest] > heap[left])) {
        smallest = left
    }
    if ((heap.length > right) && (heap[smallest] > heap[right]))
        smallest = right
    if (smallest != index) {
        var tmp = heap[smallest]
        heap[smallest] = heap[index]
        heap[index] = tmp
        minHeapify(heap, smallest)
    }
    return heap;
}

function convertMax(maxHeap) {
    for (var i = Math.floor((maxHeap.length) / 2); i > -1; i--)
        maxHeap = minHeapify(maxHeap, i)
    return maxHeap
}

****可以运行上面的代码解决方案。 我们可以将 given 视为maxHeap一个规则的元素数组并对其重新排序,以便它准确地表示一个最小堆。该函数通过在每个节点上convertMax()调用函数从最低父节点恢复所有节点上的堆属性。minHeapify()

构建堆的时间复杂度是O(n) 。这个问题也是如此。

function minHeapify(heap, index) {
    var left = index * 2;
    var right = (index * 2) + 1;
    var smallest = index;

    if ((heap.length > left) && (heap[smallest] > heap[left])) {
        smallest = left
    }
    if ((heap.length > right) && (heap[smallest] > heap[right]))
        smallest = right
    if (smallest != index) {
        var tmp = heap[smallest]
        heap[smallest] = heap[index]
        heap[index] = tmp
        minHeapify(heap, smallest)
    }
    return heap;
}

function convertMax(maxHeap) {
    for (var i = Math.floor((maxHeap.length) / 2); i > -1; i--)
        maxHeap = minHeapify(maxHeap, i)
    return maxHeap
}

var maxHeap = [9,4,7,1,-2,6,5]
console.log(convertMax(maxHeap))

-->
[ -2, 1, 4, 5, 7, 6, 9 ]