使用JavaScript深入研究数据结构 - 堆
#javascript #网络开发人员 #电脑科学 #datastructures

堆是一种基本数据结构,被构造为完整二进制树的专业形式。众所周知,它们可以有效地组织数据,以便基于“ Heap属性”快速访问最高或最低的值元素。

“堆属性”可以分为两种主要类型:最大堆和最小堆。在最大堆中,每个父节点的值大于或等于其子节点,而在分钟内,每个父节点的值小于或等于其子节点。堆被广泛用于创建有效的算法和优先队列的支持操作。它们对于许多应用都是必不可少的。

有各种形式的堆,最常见的实现是“二进制堆” - 我们将在本文中关注。本文的语气假设您至少熟悉树数据结构和二进制树的概念。如果不是这种情况,或者您需要快速茶点,我建议您从“树木概论”文章开始,然后遵循下面的链接,然后返回并稍后再继续:

Deep dive into data structures using Javascript - Introduction to Trees

二进制堆的本术

heap-binary-heap-anatomy

堆有有趣的解剖结构。它遵循完整的二进制树的结构设计 - 这意味着除最后一个可能的所有级别,除了最后一个级别,并且最后一个级别的节点都与左侧对齐。最有趣的部分是用于实现这种类似二进制树的行为的基础结构不是传统的二进制树。取而代之的是,数组被用来表示堆以允许对元素的有效访问和操纵。

每个堆都有两个重要属性:

堆订单属性:此属性确定父节点和子节点之间的关系。在最大的堆中,父母的价值超过或等于孩子。在一个分钟的堆中,父母的价值比或等于孩子的价值。

在实现方面,最小和最大堆几乎是相同的,除了一个关键区别:“比较器”。比较器是定义堆顺序的属性。通常的方法是拥有一个基本堆类,然后通过提供指定所需堆顺序的比较器来扩展它。

堆形状属性:此属性保证了形成堆的二进制树的完整性,其中所有级别(可能是最后一个级别除外)都已完全填充,最后级别的节点在从左到右的时尚。

为了维护这些属性和类似二进制的属性行为,我们通过采用各种公式来使用一个数组 - 例如确定堆中任何元素的父,左或右子。

当我们在插入或删除过程中保持堆的堆积时,它们发挥了重要作用。让我们深入了解阵列表示在实践中的工作方式。

详细的二进制堆的阵列表示

由于其有效的内存使用情况,数组中的

堆表示是一种流行的实现选择。在此表示形式中,堆中节点的位置映射到数组中的索引。为了找出类似树状的节点位置,例如父,左或右子节点,我们使用3种不同的公式:

  • parent:< / strong>使用整数除法,可以在由(i -1) / 2确定的位置找到索引(“ i”)的节点的父节点。在JavaScript中,该计算用Math.floor方法包装,例如:Math.floor((i -1) / 2)。我们需要该部门的地板价值的原因是要确保获得最接近的较低整数值。这很重要,因为数组索引是整数值,并且不能具有分数部分。
  • 左儿童:在使用公式2i + 1或(2 x i) + 1 + 1
  • 计算的位置可以找到索引(“ i”)的节点的左子女
  • 正确的孩子:在使用公式2i + 2或(2 x i) + 2
  • 计算的位置上可以找到索引(“ i”)的节点的正确子女

看下面的视觉效果,以查看所有这些公式在实践中如何工作的基本示例:

binary-heap-array-representation

堆订单如何保持维护

通过称为“堆”的过程来维护堆的顺序。有两种类型的堆操作:hupifyup(也称为siftup或bubbleup),通常在插入后进行,而hepifydown(也称为siftdown或bubbledown),通常是在拆除后完成的。

>

插入和堆积

将新元素插入堆中时,它最初位于堆的数组表示中的下一个可用位置(这是数组的末端)。 bï»»ut这可能会违反堆的属性,因为新插入的元素mï»»比其父母更大(或者在最小的堆中)。

要恢复堆属性,请执行heapifyUp操作。在heapifyUp期间,我们将插入元素与其父母进行比较。如果插入的元素比最小堆(或最大堆中较大)的父元素小,则我们将其与其父元素交换。我们继续进行此过程,直到恢复堆属性为止,即每个父节点都比其子节点更大(或小时)。

in更简单的术语:

  • 我们将元素插入数组的末尾。
  • 我们通过从最后一个节点开始就燃烧新插入的元素。
  • 我们在每个步骤上采取2个操作:首先,我们将一个节点与父母进行比较,并在必要时交换其值,然后移至下一个父母,直到我们到达根节点。

tï»»ake acï»»失败者看着gifbï»»elow vish»»ialize heapifyUp在min堆中的实践中如何工作:

min-heap-insert-heapify-up

- 视觉生成:âhttps://www.cs.usfca.edu/~galles/visualization/Heap.html

拆卸和堆

从堆中删除元素时,我们通常会删除根部元素,因为可以在恒定时间内执行此操作。 bï»»这在堆的根部留下了一个孔。为了填补这一空白,我们将堆中的最后一个元素移至根位置。这可能会违反堆属性,因为此元素元素可能比其孩子大(或者在最大堆中)更大。

要恢复堆属性,请执行heapifyDown操作。在heapifyDown期间,我们将根元素与孩子进行比较。如果根部元素大于一个分钟堆(或在最大堆中较小)中的两个孩子,则我们将其与较小的(或最大堆)儿童更小(或更大的孩子)交换。我们继续沿堆的过程继续进行此过程。

这些堆积的操作确保堆保持其订单并始终满足堆属性,无论我们是添加新元素还是从堆中删除现有元素。通过使用数组表示形式以及heapifyUpheapifyDown操作,我们可以保证堆保持平衡,从而使插入,删除,并找到最小/最大元素等操作。

tï»»ake acï»»gifbï»»elow elow vish»»quatize heapifyDown在实践中如何在一个分钟内工作:

heap-remove-heapify-down

- 视觉生成:âhttps://www.cs.usfca.edu/~galles/visualization/Heap.html

何时使用堆

堆具有独特的属性,使其在特定情况下有用。 lï»et eet首先要查看堆中的Big O的常见操作:

heap-binary-heap-time-complexity

插入操作(插入):需要O(log n)时间复杂性。在最坏的情况下,我们可能必须在插入时抬高根部(堆的高度)。

删除操作(删除):也需要O(log n)时间复杂性。删除节点可能需要从根部到叶节点遍历堆。

获得最小/最大值(PEEK):需要O(1)时间复杂性。最小或最大元素(取决于堆类型)始终处于根部。

sï»»earch(可选):搜索不是堆的主要目标,因为它主要用于维持快速访问最小值或最大值。在搜索的情况下,由于我们使用一个数组,时间复杂性将与数组相同 - 是O(n)

考虑到这些时间复杂性分析,在以下情况下,堆非常有用:

优先队列:堆是实现优先级队列的首选数据结构。优先队列是一种特殊类型的队列,每个元素都有优先级,并根据优先级提供服务。

堆排序: hepsort是一种使用二进制堆数据结构的排序算法。它具有O(n log n)的时间复杂性,这使得大型数据集有效。

图形算法:堆被广泛用于诸如Dijkstra算法和PRIM的算法之类的图算法中。这些算法需要一个可以快速提供最小或最大元素的数据结构,该算法有效地处理。

k'th最大/最小的元素:堆可以有效地解决与查找数组中最小或最大元素有关的问题。这涉及建造最小堆或最大堆并执行删除操作k times。

滑动窗口问题:堆在需要在滑动窗口中找到最大/最小元素的问题有帮助。他们可以在对数时间中执行插入和删除。

JavaScript中的堆实现

我们将使用ES6类来建造堆。还包括MinHeapMaxHeap子类 - 它们从Heap类继承。 MinHeap使用一个比较器,该比较器按升序排列堆,而MaxHeap使用的比较器以降序排列堆。

这是我们要实现的方法列表:

  • size():返回堆中的项目数
  • isEmpty():检查堆是否为空。
  • peek():返回堆中的顶部元素(最小值或最大值)而无需删除。
  • insert(value):为堆增加了一个新值并维护堆属性。这是将元素添加到堆中的基本操作。
  • delete():在维护堆属性的同时,删除并返回堆中的顶部元素(最小值或最大值)。这是从堆中删除元素的基本操作。
  • parentIndex(i)parentValue(i)leftChildIndex(i)leftChildValue(i)hasLeftChild(i)rightChildIndex(i)rightChildValue(i)hasRightChild(i):用于导航堆结构的帮助方法。它们与计算索引并访问父和子节点索引或值有关。
  • heapifyUp():添加新元素后重新布置堆。它确保维护堆属性并与插入过程相关。
  • heapifyDown():将节点向下移动,直到其位置正确。它确保维护堆属性并与删除过程相关。
  • displayHeap(heap):以级别排序显示堆为二进制树。这对于可视化和调试目的很有用。

hï»»eaps不是最先进的数据结构,但是在开始时它们仍然很难理解。我的建议首先要熟悉其对二进制树的数组表示,其中包括如何找到父母,左或右子的任何元素的左或右子,youyï»»唱歌aï»»rray索引公式。然后仔细研究hï»»ow的机制确实插入并删除了利用hepifyup和hepifydown方法。

iï»»f您想了解一种实用的方式,我强烈建议您在下面的链接上与他的出色可视化器一起玩:

https://www.cs.usfca.edu/~galles/visualization/Heap.html

此外,我还将每种方法的逐条解释都包括在实施中,以便您跟进代码中发生的事情。以及他们的工作方式!我想鼓励您在您喜欢的代码编辑器中尝试以下实现。感谢您的阅读!

javaScript中的堆的mplementation

class Heap {
  // The constructor method is called when a new instance of Heap is created.
  constructor(comparator) {
    // Initialize the heap array.
    this.heap = [];

    // Set the comparator method for comparing nodes in the heap.
    // If no comparator function is provided, it defaults to a comparison
    // function that sorts in ascending order (a min-heap).
    this.comparator = comparator || ((a, b) => a - b);
  }

  // Return the number of items in the heap.
  size() {
    return this.heap.length;
  }

  // Check if the heap is empty.
  isEmpty() {
    return this.size() == 0;
  }

  // Get the top element in the heap without removing it.
  // For a min-heap, this will be the smallest element;
  // for a max-heap, it will be the largest.
  peek() {
    return this.heap[0];
  }

  // Add a new value to the heap.
  insert(value) {
    // First, add the new value to the end of the array.
    this.heap.push(value);

    // Then, move the new value up the heap to its correct position.
    this.heapifyUp();
  }

  // Remove and return the top element in the heap.
  delete() {
    // If the heap is empty, just return null
    if (this.isEmpty()) {
      return null;
    }
    // Save the top element so we can return it later.
    const poppedValue = this.peek();

    // If there is more than one node in the heap, move the last node to the top.
    const bottom = this.size() - 1;
    if (bottom > 0) {
      this.swap(0, bottom);
    }

    // Remove the last node (which is now the top node) from the heap.
    this.heap.pop();

    // Move the new top node down the heap to its correct position.
    this.heapifyDown();

    // Finally, return the original top element.
    return poppedValue;
  }

  // Method to get the index of a node's parent.
  parentIndex(i) {
    /*
    About Math.floor:

    We take the floor value of the division to 
    make sure we get the nearest lower integer value. 
    This is important because array indexes
    are integer values and cannot have fractional parts.
    */
    return Math.floor((i - 1) / 2);
  }

  // Method to get the value of a node's parent.
  parentValue(i) {
    /*
   Check if the index is within the bounds of the heap and the parent exists. 
   If the index is less than the heap's size and the parent index is greater than or equal to 0, it means the parent exists. 
   If it doesn't exist or the index is out of bounds, we return undefined.
  */
    return i < this.size() && this.parentIndex(i) >= 0
      ? this.heap[this.parentIndex(i)]
      : undefined;
  }

  // Method to get the index of a node's left child.
  leftChildIndex(i) {
    return 2 * i + 1;
  }

  // Method to get the value of a node's left child.
  leftChildValue(i) {
    /*
   Check if the left child exists. 
   If the left child index is less than the size of the heap, it means the left child exists. 
   If it doesn't exist, we return undefined.
  */
    return this.hasLeftChild(i) ? this.heap[this.leftChildIndex(i)] : undefined;
  }

  // Method to check if a node has left child.
  // It returns true if the left child index is within the valid range of heap indexes, 
  // which indicates that a left child exists.
  hasLeftChild(i) {
    return this.leftChildIndex(i) < this.size();
  }

  // Method to get the index of a node's right child.
  rightChildIndex(i) {
    return 2 * i + 2;
  }

  // Method to get the value of a node's right child.
  rightChildValue(i) {
    /*
     Check if the right child exists. 
     If the right child index is less than the size of the heap, it means the right child exists. 
     If it doesn't exist, we return undefined.
    */
    return this.hasRightChild(i)
      ? this.heap[this.rightChildIndex(i)]
      : undefined;
  }

  // Method to check if a node has right child.
  // It returns true if the right child index is within the valid range of heap indexes, 
  // which indicates that a right child exists.
  hasRightChild(i) {
    return this.rightChildIndex(i) < this.size();
  }

  // Method to swap the values of two nodes in the heap.
  swap(i, j) {
    // Swap the values of elements at indices i and j without using a temporary variable:
    [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
  }

  // This method is used to rearrange the heap after adding a new element.
  heapifyUp() {
    // We start from the last node in the heap. This is the node that was most recently added.
    let nodeIndex = this.size() - 1;

    /*
    In the while loop, we swap the values to maintain the heap property.
    When? If the following 2 conditions are true:

    - "nodeIndex > 0": 
      This means we haven't reached to the main parent yet.

    - "this.comparator(parentNodeValue, currentNodeValue) > 0":
      If this is true, it means the heap property is violated and we need to swap values.
      "comparator" function here compares if the current node is smaller or bigger than it's parent. 
      Based on the heap order, comparator function will be slightly different. Such as:
        ((a, b) => (a - b) for min heap
        ((a, b) => (b - a) for max heap
    */

    while (
      // The node is not the root (it has a parent).
      nodeIndex > 0 &&
      this.comparator(this.parentValue(nodeIndex), this.heap[nodeIndex]) > 0
    ) {
      // The heap property is violated for our node and its parent. We need to swap them to restore the heap property.
      this.swap(nodeIndex, this.parentIndex(nodeIndex));

      // After swapping, our node has moved one level up the heap. We update nodeIndex to the index of the parent.
      // In the next iteration of the loop, we'll check the heap property for the node and its new parent.
      nodeIndex = this.parentIndex(nodeIndex);
    }
  }

  // Method to move a node down the heap until it is in the correct position.
  // Used after a deletion
  heapifyDown() {
    // We start from the top node in the heap which is the root, always at index 0.
    // So we initialize the current node index at 0.
    let currNodeIndex = 0;

    // The 'while' loop continues as long as the current node has a left child.
    // The reason for this is that a heap is a complete binary tree and hence,
    // a node would have a left child before it has a right child.
    while (this.hasLeftChild(currNodeIndex)) {
      // Assume the smaller child is the left one.
      let smallerChildIndex = this.leftChildIndex(currNodeIndex);

      /*
      Check if there's a right child and compare it to the left child.
      If the right child is smaller, update the smallerChildIndex to the right child's index
      Based on the heap order, comparator function will be slightly different. Such as:
        ((a, b) => (a - b) for min heap
        ((a, b) => (b - a) for max heap
      */
      if (
        this.hasRightChild(currNodeIndex) &&
        this.comparator(
          this.rightChildValue(currNodeIndex),
          this.leftChildValue(currNodeIndex)
        ) < 0
      ) {
        smallerChildIndex = this.rightChildIndex(currNodeIndex);
      }

      // If the current node is smaller or equal to its smaller child, then it's already in correct place.
      // This means the heap property is maintained, so we break the loop.
      if (
        this.comparator(
          this.heap[currNodeIndex],
          this.heap[smallerChildIndex]
        ) <= 0
      ) {
        break;
      }

      // If the current node is not in its correct place (i.e., it's larger than its smaller child),
      // then we swap the current node and its smaller child.
      this.swap(currNodeIndex, smallerChildIndex);

      // After swapping, the current node moves down to the position of its smaller child.
      // Update the current node index to the smaller child's index for the next iteration of the loop.
      currNodeIndex = smallerChildIndex;
    }
  }

  // Displays an array that will represent the heap as a binary tree
  // in level-order, with each sub-array representing a level of the tree.
  // such as: [ [ 1 ], [ 5, 3 ], [ 9, 6, 8 ] ]
  displayHeap() {
    let result = [];
    let levelCount = 1; // Counter for how many elements should be on the current level.
    let currentLevel = []; // Temporary storage for elements on the current level.

    for (let i = 0; i < this.size(); i++) {
      currentLevel.push(this.heap[i]);

      // If the current level is full (based on levelCount), add it to the result and reset currentLevel.
      if (currentLevel.length === levelCount) {
        result.push(currentLevel);
        currentLevel = [];
        levelCount *= 2; // The number of elements on each level doubles as we move down the tree.
      }
    }

    // If there are any elements remaining in currentLevel after exiting the loop, add them to the result.
    if (currentLevel.length > 0) {
      result.push(currentLevel);
    }

    return result;
  }
}

class MinHeap extends Heap {
  constructor() {
    super((a, b) => a - b); // MinHeap uses a comparator that sorts in ascending order
  }
}

class MaxHeap extends Heap {
  constructor() {
    super((a, b) => b - a); // MaxHeap uses a comparator that sorts in descending order
  }
}