排序算法-选择排序-堆排序

163 阅读6分钟

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战

堆排序

介绍

堆排序 这是十大排序算法里 较难的其中一种排序,牵扯的知识点很多。它是选择排序中的一种,它的排序算法思想,是根据 数据结构中的 来设计的。

那想了解什么是 数据结构中的 是什么,可以看下之前写的文章 数据结构-堆 及 相关代码 数据结构-堆-代码讲解

下面是 堆排序 动态图的一个演示

d2f3f4bc776b7d45daa73cad9f9370a0.gif

步骤点

由上面的演示图,我这里将 堆排序 分为以下 10个 步骤点

  • 插入元素

  • 产生堆

  • 交换值: 根节点 与 最后一个叶子节点

  • 删除: 最新数组 的最后一个元素

  • 将删除的元素,unshift 到一个新数组中

  • 从现在数组的 根节点开始

  • 比较:根节点 与 左右子孩子 进行比较

  • 交换

  • 产生新的堆

  • 循环结束

讲解

随机 数组的值为 [7,15,1,20,2,9,3,6],图如下

image.png

1. 插入元素

首先,堆排序,肯定是我们先给定一个随机的数组,然后让其生成为一个 结构。

插入元素,我们需要将这个随机数组中的元素,依次进行遍历,将元素进行插入操作

  const arr = [7, 15, 1, 20, 2, 9, 3, 6]
  arr.forEach((item) => {
    insert(item)
  })

2. 产生堆

插入后的元素,从下往上,循环找到父元素,进行 当前元素 与 父元素 的比对后,找到其中最大值,交换,并重新更新赋值 新元素的下标。来产生 结构。

通过 insert 这个方法,我们将会得到的新数组为 [20,15,9,7,2,1,3,6],是一个 结构。

// 插入新元素
const insert = (value) => {
  dataArray.push(value)

  // 获取 当前元素 的 下标
  let currentIndex = dataArray.length - 1

  // 获取 当前元素 的 父元素 下标
  let currentFatherIndex = getFatherIndex(currentIndex)

  // 判断 元素的父元素的下标是否存在
  while (currentFatherIndex >= 0) {
    // 如果 当前元素 的值 大于 当前元素的父元素 的值,进行交换操作
    if (compareFunc(currentIndex, currentFatherIndex)) {
      exchangeFunc(currentIndex, currentFatherIndex)
    }

    // 更新 当前元素下标 和 父元素下标
    currentIndex = currentFatherIndex
    currentFatherIndex = getFatherIndex(currentIndex)
  }
}

3. 交换值

从上面两步骤,得到数组结果为 [20,15,9,7,2,1,3,6]

接下来 将此时的 根节点(数组中的第一个值,下标 0) 与 最后一个叶子节点(数组中的最后一个值,下标 dataArrayLength - 1) 进行交换值。

交换后的数组为:[6,15,9,7,2,1,3,20]

const exchangeFunc = (A, B) => {
  const temp = dataArray[B]
  dataArray[B] = dataArray[A]
  dataArray[A] = temp
}

4. 删除

删除 现在这个数组中的 最后一个元素(也就是刚开始的根节点)

删除后的数组为:[6,15,9,7,2,1,3]

    dataArray.pop()

5. unshift 到 排序数组中

pop 出来的元素,unshift 到 我们要的排序数组中 [20]

    dataSortArray.unshift(dataArray.pop())

这里注意,这里我默认是 从小到大 进行排序,如果想要进行 从大到小 排序,只需将代码中的 unshift 变成 push 即可。

6. 从现在数组的 根节点开始

这里从 数组的 根节点开始,从上往下,找到 左右两个子孩子。

这里从根节点开始查找的原因,是因为上面的步骤所导致的。交换后,又把最大值 pop 出去了。那么现在剩下的数组肯定不是 结构,我们把对其产生影响的元素(根元素),重新的进行 比较,让它回到自己应该在的位置后,得到新的 结构。

7. 比较:根节点 与 左右子孩子 进行比较

我们知道当前根元素的下标,那么自然通过下面两个公式,来获得左右孩子的下标,

  • 左孩子: 2*i+1
  • 右孩子: 2*i+2

目前这三个下标 所对应的值 进行比对,找到最大值下标。

这里注意,需要查看左右孩子是否存在,如果不存在的话,那么我们就不需要进行比对了。

    // 获取 当前元素 的 左边孩子下标
    const getLeftChildIndex = (index) => {
      return 2 * index + 1
    }

    // 获取 当前元素 的 右边孩子下标
    const getRightChildIndex = (index) => {
      return 2 * index + 2
    }
    // 定义左右孩子最大值的下标
    let maxIndex = -1

    // 查看左孩子是否存在
    if (existenceFunc(currentLeftChildIndex, dataArrayLength - 1)) {
      // 左孩子存在,查看右孩子是否存在
      if (existenceFunc(currentRightChildIndex, dataArrayLength - 1)) {
        // 进行 左孩子 和 右孩子 值 大小比较,获得最大值的 元素下标
        maxIndex = compareFunc(currentLeftChildIndex, currentRightChildIndex) ? currentLeftChildIndex : currentRightChildIndex
      } else {
        // 只有左孩子存在
        maxIndex = currentLeftChildIndex
      }
    }

8. 交换

将最大值的值,与 当前元素的值进行交换,并且更新当前元素的下标。

    if (maxIndex >= 0 && compareFunc(maxIndex, currentIndex)) {
      // 大于则 交换
      exchangeFunc(maxIndex, currentIndex)
    }

9. 产生新的堆

依次往下查找,直到当前元素为叶子结点,这样我们就会得到一个新的堆 及 排序数组的结果。

新堆结果为: [15,7,9,6,2,1,3]

排序数组结果为:[20]

  // 查找条件
  while (currentIndex >= 0 && currentIndex <= dataArrayLength - 1) {}

10. 循环结束

判断 当前数组中,是否还有数据,直至为空。

循环的堆结构:

  • 初始化:[20, 15, 9, 7, 2, 1, 3, 6]
  • 第一次 堆排序: [15, 7, 9, 6, 2, 1, 3]
  • 第二次 堆排序:[ 9, 7, 3, 6, 2, 1 ]
  • 第三次 堆排序:[ 7, 6, 3, 1, 2 ]
  • 第四次 堆排序:[ 6, 2, 3, 1 ]
  • 第五次 堆排序:[ 3, 2, 1 ]
  • 第六次 堆排序:[ 2, 1 ]
  • 第七次 堆排序:[ 1 ]
  • 第八次 堆排序:[]

循环后的排序数组:[1, 2, 3, 6, 7, 9, 15, 20]

这是上图 gif 的最终结果,可以很明显看到 最上到下 从左到右 的排序结果。

image.png

总结

堆排序 ,作为 选择排序 中的一种,它涉及到 数据结构 中众多的知识点。

这里有我自己的一个总结理解就是:

形成 结构,那么我们就需要从最后一个元素,进行循环对比,来将最大的值赋值到根节点处。这也就是我们常说的 大顶堆

堆排序,就是 现将根节点的值和最后一个元素的值进行交换,再把最后一个元素的值删除,并放到排序数组中。然后将删除后的数组 再次打造为一个 大顶堆。 依次循环,直到结束后。我们得到的排序数组的结果 就是一个 小顶堆