了解最小堆与最大堆

137 阅读5分钟

了解最小堆与最大堆

堆是一种基于树的数据结构,允许在恒定时间内访问树中的最小和最大元素。所用的恒定时间是大O(1)。这与存储在堆中的数据无关。

有两种类型的堆。最小堆和最大堆。Min-heap用于访问堆中的最小元素,而Max-heap则用于访问堆中的最大元素。

前提条件

要跟上这篇文章,你应该具备以下条件。

  • 在你的电脑上安装[Node.js]。

  • 有JavaScript的基本知识。

建立项目

要建立这个项目,请克隆这个GitHub仓库。克隆后,你会发现两个文件夹:startfinal。在本文中,我们将在start文件夹中工作,但如果你遇到了错误,请随时查看final文件夹中的最终代码。

迷你堆

在最小堆中,父节点或根节点通常比子节点的价值要小。最小的元素在恒定的时间内被访问,因为它的索引是1

min-heap

根据下图,在每一级,最小的数字是父节点。

实施

在说明最小堆的时候,我们使用的是基于树的结构。但当存储在内存中时,我们使用基于数组的结构。请看下图,它显示了基于树的和基于内存的表示方法。

min-heap-implementation

在迷你堆中,第一个元素是null ,然后用下面的公式来排列元素。

  • 父节点。i

  • 左结点。i * 2

  • 右节点。i * 2 + 1

  • 在任何一个节点,你都可以通过以下方式找到父节点i / 2

  • i 是数组中的索引。

对于最小堆,我们将插入一个元素,获得最低的元素,并删除一个元素。

插入一个元素

在迷你堆中插入一个元素时,我们使用堆排序算法

该算法的工作原理是,首先将要插入的元素推到数组的末尾,然后通过遍历为该元素找到正确的位置。

minHeap.js 文件中,在insert() 函数下,我们增加了以下功能来插入一个元素。

function insert(node) {
  heap.push(node);

  if (heap.length > 1) {
    let current = heap.length - 1;

    while (current > 1 && heap[Math.floor(current / 2)] > heap[current]) {
      //swapping values
      [heap[Math.floor(current / 2)], heap[current]] = [
        heap[current],
        heap[Math.floor(current / 2)],
      ];

      current = Math.floor(current / 2);
    }
  }

  return;
}

//testing functionality

insert(10);
insert(90);
insert(36);
insert(5);
insert(1);

console.log(heap.slice(1));

预期的输出。

[ 1, 5, 36, 90, 10 ]

从上面的函数中。

  • 将元素推到数组的末端。

  • 检查数组中的元素数量是否多于1。如果是,就按照下面的步骤进行。

  • 获取被插入元素的索引。

  • 在数组中循环,检查是否有一个比插入的元素大的父元素。

  • 如果存在,就把它们交换。

检索最小元素

在min-heap数据结构中,最小元素的索引是1

在同一个文件中,在getMin() 函数下,我们把功能加起来。

function getMin(){
    return heap[1];
};

//testing functionality

insert(10);
insert(90);
insert(36);
insert(5);
insert(1);

console.log(getMin());

预期的输出。

1

从上面的代码片断中,我们得到了存储在索引1 的最小元素。

移除一个元素

从最小堆数据结构中删除一个元素包括以下步骤。

  • 首先删除最小的元素。

  • 调整min-heap以保留顺序。

在同一个文件中,在remove() ,我们把这些功能加起来。

function remove() {
  if (heap.length > 2) {
    //assign last value to first index
    heap[1] = heap[heap.length - 1];

    //remove the last value
    heap.splice(heap.length - 1);

    if (heap.length === 3) {
      if (heap[1] > heap[2]) {
        //swap them
        [heap[1], heap[2]] = [heap[2], heap[1]];
      }
      return;
    }

    //get indexes
    let parent_node = 1;
    let left_node = parent_node * 2;
    let right_node = parent_node * 2 + 1;

    while (heap[left_node] && heap[right_node]) {
      //parent node greater than left child node
      if (heap[parent_node] > heap[left_node]) {
        //swap the values

        [heap[parent_node], heap[left_node]] = [
          heap[left_node],
          heap[parent_node],
        ];
      }

      //parent node greater than right child node
      if (heap[parent_node] > heap[right_node]) {
        // swap
        [heap[parent_node], heap[right_node]] = [
          heap[right_node],
          heap[parent_node],
        ];
      }

      if (heap[left_node] > heap[right_node]) {
        //swap
        [heap[left_node], heap[right_node]] = [
          heap[right_node],
          heap[left_node],
        ];
      }

      parent_node += 1;
      left_node = parent_node * 2;
      right_node = parent_node * 2 + 1;
    }

    //incase right child index is undefined.
    if (heap[right_node] === undefined && heap[left_node] < heap[parent_node]) {
      //swap.
      [heap[parent_node], heap[left_node]] = [
        heap[left_node],
        heap[parent_node],
      ];
    }
  }

  // if there are only two elements in the array.
  else if (heap.length === 2) {
    // remove the 1st index value
    heap.splice(1, 1);
  } else {
    return null;
  }

  return;
}

//testing functionality

insert(10);
insert(90);
insert(36);
insert(5);
insert(1);

remove();

console.log(heap.slice(1));

预期的输出

[ 5, 10, 36, 90 ]

从上面的函数中。

  • 检查数组是否有两个以上的元素。如果没有,删除第一个索引中的元素。如果有,继续下面的步骤。

  • 将最后一个值分配给第一个索引。

  • 从数组中删除最后一个值。

  • 检查该数组是否还有三个元素。如果是true ,检查第一个元素是否比第二个元素大。如果满足条件,就把它们交换。如果有三个以上的元素,继续下面的步骤。

  • 定义父结点、左结点和右结点的索引。

  • 循环浏览同时具有左子值和右子值的数组。如果父节点的值大于左节点的值或右节点的值,将它们交换。如果左节点的值大于右节点的值,也将它们交换。

  • 如果没有右节点的值,但父节点的值大于左节点的值,则交换它们的值。

Max-heap

在一个最大堆中,父节点或根节点通常比子节点大。最大元素可以在恒定的时间内被访问,因为它的索引是1

max-heap

根据上图,在每一级,最大的数字是父节点。

实施

同样地,当说明最大堆时,我们使用基于树的结构,但当在内存中表示时,我们使用基于数组的结构。请看下图,它显示了基于树的和基于内存的表示。

max-heap-implementation

类似地,在一个最大堆中,第一个元素是null ,然后在排列元素时使用以下公式。

  • 父节点。i

  • 左边的节点。i * 2

  • 右边的节点。i * 2 + 1

  • 在任何节点,你都可以通过以下方式找到父节点i / 2

i 是数组中的索引。

对于最大堆,我们将插入一个元素,得到最大的元素,并删除一个元素。

插入一个元素

在最大堆中,我们在插入元素时也使用堆排序算法。

maxHeap.js 文件中,在insert() 函数下,我们增加了以下功能来插入元素。

function insert(node) {
  //insert first at the end of the array.
  heap.push(node);

  if (heap.length > 1) {
    //get index
    let current = heap.length - 1;

    //Loop through checking if the parent is less.

    while (current > 1 && heap[Math.floor(current / 2)] < heap[current]) {
      //swap
      [heap[Math.floor(current / 2)], heap[current]] = [
        heap[current],
        heap[Math.floor(current / 2)],
      ];

      //change the index
      current = Math.floor(current / 2);
    }
  }
}

//testing functionality

insert(10);
insert(100);
insert(120);
insert(1000);

console.log(heap.slice(1));

预期的输出。

[ 1000, 120, 100, 10 ]

从上面的函数中。

  • 将元素推到数组的末端。

  • 检查数组中是否有多于一个元素。如果有,继续下面的步骤。

  • 获取该元素的位置索引。

  • 在数组中循环,检查是否有一个父节点的值小于插入的元素。

  • 如果有,交换数值并更新数组中元素的索引。

获取最大的元素

在一个最大堆中,获取最大的元素意味着访问索引为1 的元素。

在同一个文件中,在getMax() 函数下,我们把这些功能加起来。

function getMax(){
    return heap[1];
};

//testing functionality

insert(10);
insert(100);
insert(120);
insert(1000);

console.log(getMax());

预期的输出。

1000

在上面的函数中,我们要返回存储在索引1 的元素。

移除一个元素

从一个最大堆中移除一个元素包括以下步骤。

  • 移除第一个元素,它通常是最大的。

  • 按顺序重新排列其余的元素。

在同一个文件中,在remove() 函数下,我们将把这些功能加起来。

function remove() {
  //check if we got two elements in heap array.
  if (heap.length === 2) {
    //remove the Ist index value
    heap.splice(1, 1);
  } else if (heap.length > 2) {
    //assign last value to first index
    heap[1] = heap[heap.length - 1];

    //remove the last item
    heap.splice(heap.length - 1);

    //check if the length is 3.
    if (heap.length === 3) {
      if (heap[2] > heap[1]) {
        [heap[1], heap[2]] = [heap[2], heap[1]];
      }
    }

    //setup needed indexes.
    let parent_node = 1;
    let left_node = parent_node * 2;
    let right_node = parent_node * 2 + 1;

    while (heap[left_node] && heap[right_node]) {
      //parent node value is smaller than the left node value
      if (heap[left_node] > heap[parent_node]) {
        //swap
        [heap[parent_node], heap[left_node]] = [
          heap[left_node],
          heap[parent_node],
        ];

        //update the parent node index.
        current = left_node;
      }

      if (heap[right_node] > heap[parent_node]) {
        //swap
        [heap[parent_node], heap[right_node]] = [
          heap[right_node],
          heap[parent_node],
        ];

        //update the parent node index.
        current = right_node;
      }

      if (heap[left_node] < heap[right_node]) {
        //swap
        [heap[left_node], heap[right_node]] = [
          heap[right_node],
          heap[left_node],
        ];
      }

      //update the left and right node
      left_node = current * 2;
      right_node = current * 2 + 1;
    }

    //no right child, but left child is greater than parent
    if (heap[right_node] === undefined && heap[left_node] > heap[parent_node]) {
      //swap
      [heap[parent_node], heap[left_node]] = [
        heap[left_node],
        heap[parent_node],
      ];
    }
  } else {
    return null;
  }

  return;
}

//testing functionality

insert(10);
insert(100);
insert(120);
insert(1000);

remove();

console.log(heap.slice(1));

从上面的函数中。

  • 检查数组是否有两个以上的元素。如果没有,只需删除第一个索引中的元素。如果有,继续下面的步骤。

  • 将最后一个值分配给第一个索引。

  • 从数组中删除最后一个值。

  • 检查该数组是否还有三个元素。如果是true ,检查索引二的元素是否比索引一的元素大。如果满足条件,将它们交换。如果剩余的元素超过三个,继续下面的步骤。

  • 定义父结点、左结点和右结点的索引。

  • 循环浏览有左节点值和右节点值的数组。如果父节点的值比左节点或右节点的值小,就把它们交换。另外,如果左节点的值小于右节点的值,就把它们交换一下。

  • 如果没有右节点的值,但父节点的值小于左节点的值,则交换它们的值。

为什么我们需要堆?

  • 降低时间复杂度。线性数据结构,如链接列表或数组可以访问大O(n)中存在的最小或最大元素,而堆可以访问大O(1)中存在的最小或最大元素。

这在处理大数据集时是至关重要的。n指的是数据集的数量。

堆的应用

  • 它们已经被用于[操作系统]中,在优先级的基础上[进行工作调度]。

  • 它们被用于[堆排序算法]中,以实现优先级队列。

  • 在[Dijkstra算法]中用于寻找最短路径。

结论

随着时间复杂度的降低,min-heap和max-heap在处理数据集时很有效率,每一种都有自己的用例和实现。

在这篇文章中,我们已经涵盖了迷你堆、最大堆、我们为什么需要堆,以及堆的应用。