排序算法:堆排序的理解与实现

前言

堆排序相比冒泡排序、选择排序、插入排序而言,排序效率是最高的,本文从堆的属性和特色出发采用图文形式进行讲解并用JavaScript将其实现,欢迎各位感兴趣的开发者阅读本文😝javascript

堆属性

堆分为两种: 最大堆和最小堆,二者的差异在于节点的排序方式。java

在不一样类型的堆中,每个节点都遵循堆的属性,下方所述内容即为堆的属性。api

  • 最大堆: 父节点的值大于子节点的值
  • 最小堆: 父节点的值小于子节点的值

由一个彻底二叉树组成,且树中的全部节点都知足堆属性,这个彻底二叉树就是堆。数组

根据堆的属性可知:数据结构

堆的根节点中存放的是最大或最小元素,可是其余节点的排列顺序是未知的。 例如,在一个最大堆中,最大的那一个元素老是位于index0的位置,可是最小的元素则未必是最后一个元素,惟一可以保证的是最小的元素是一个叶节点,可是不肯定是哪个。函数

  • 最大堆根节点中的元素必定是树中的最大值
  • 最小堆根节点中的元素必定是树中的最小值

数组实现堆

用数组来实现堆,堆中的节点在数组的位置与它的父节点以及子节点的索引之间有一个映射关系。post

用公式来描述当前节点的父节点和子节点在数组中的位置(i为当前节点的索引)测试

// 父节点的位置,向下取整(floor)
parent(i) = floor( i - 1 ) / 2)
// 左子节点的位置
left(i) = 2i +1
// 右子节点的位置,左右节点老是处于相邻的位置
right(i) = left(i)+1 = 2i+2
复制代码
  • 若计算出的索引不是一个有效的数组索引(小于0时),则当前节点没有父节点
  • 若计算出的索引大于数组的长度,则当前节点没有子节点。
  • 父节点的值必定大于等于其子节点的值,即:
array[parent(i)] >= array[i]
复制代码
  • 当前层级全部的节点未填满以前不容许开始下一层的填充

堆属性计算

堆是一个彻底二叉树,树的高度是指从树的根节点到最低叶节点所须要的步数。ui

树高度正式的定义: 节点之间的边的最大值。spa

  • 一个高度为h的堆有h+1层。

  • 若是一个堆有n个节点,那么它的高度为 floor(log2(n))

// 例如,一个堆有15个节点,求这个堆的高度
h = floor(log2(15)) = floor(3.91) = 3
套入公式可得,堆的高度为3
// log2(n),log为求次方运算,log以2为底,15的对数。若n为8,则运算结果为3,即2^3=8
复制代码
  • 若是最下面一层已经填满,那这一层的节点为 2^h个。
// 高度为3,即最下面一层有8个节点
2^3 = 8;
复制代码
  • 最下面已经填满的那一层以上的全部节点数目为 2^h -1个。
// 高度为3,即其余层的全部节点有7个
2^3 -1 = 7;
复制代码
  • 整个堆中的节点数为: 2^(h+1)-1
// 高度为3,即堆中的节点数为15
2^4 -1 = 16 - 1 = 15;
复制代码
  • 叶结点老是位于数组的floor(n/2)和n-1之间

用JS实现堆排序

实现堆排序以前,咱们须要先将即将排序的数据构建成一个最大堆,构建完成后,根据最大堆的属性可知,堆顶部的值最大,咱们将它取出,而后从新构建堆,直到堆中的全部数据被取出,堆排序也就完成了。

调整彻底二叉树中的树为一个最大堆

在一个彻底二叉树中,从一个节点出发,找到它的左子树和右子树,将当前节点与它的两颗子树进行大小比较,找到两颗树中较大的一方,将其与当前节点进行交换,交换完毕后,当前节点所在的树就是一个最大堆。咱们称这个交换操做为heapify

接下来,咱们来整理下实现思路

  • 声明一个函数,参数为: 树,树的节点数,当前要进行heapify操做的节点
  • 根据数组实现堆中所讲的,父节点和子节点在数组中位置的计算公式,找到当前节点的左子树和右子树的位置
  • 假设最大值为当前要操做的节点,将最大值与左子树和右子树分别进行大小比较
  • 进行大小比较后,若是最大值的位置不是当前操做节点的位置,则将其与当前操做节点位置的数据进行互换,递归调用heapify函数
  • 退出递归: 当前操做的节点大于等于树的总节点数时,则退出递归。
  • 当前节点与左子树和右子树进行大小比较时,若是左、右子树的位置大于树的总节点数时,不由笑最大值赋值。

接下来咱们将上述思路转化为代码:

/* * 1. 从一个节点出发 * 2. 从它的左子树和右子树中选择一个较大值 * 3. 将较大值与这个节点进行位置交换 * 上述步骤,就是一次heapify的操做 * */

// n为树的节点数,i为当前操做的节点 (找到这颗树里的最大节点)
const heapify = function (tree, n, i) {
    if(i >= n){
        // 结束递归
        return;
    }
    // 找到左子树的位置
    let leftNode = 2 * i + 1;
    // 找到右子树的位置
    let rightNode = 2 * i +2;

    /* 1. 找到左子树和右子树位置后,必须确保它小于树的总节点数 2. 已知当前节点与它的左子树与右子树的位置,找到最大值 */
    // 设最大值的位置为i
    let max = i;
    // 若是左子树的值大于当前节点的值则最大值的位置就为左子树的位置
    if(leftNode < n && tree[leftNode] > tree[max]){
        max = leftNode;
    }
    // 若是右子树的值大于当前节点的值则最大值的位置就为右子树的位置
    if(rightNode < n && tree[rightNode] > tree[max]){
        max = rightNode;
    }

    /* * 1. 进行大小比较后,若是最大值的位置不是刚开始设的i,则将最大值与当前节点进行位置互换 * */
    if(max !== i){
        // 交换位置
        swap(tree,max,i);
        // 递归调用,继续进行heapify操做
        heapify(tree,n,max)
    }
};

// 交换数组位置函数
const swap = function (arr,max,i) {
    [arr[max],arr[i]] = [arr[i],arr[max]];
};

复制代码

接下来咱们测试下heapify函数

const dataArr = [23,15,34,11,23,4,19,80];
// 咱们假设当前操做节点为数组的0号元素,咱们对0号元素进行一次heapify才作
heapify(dataArr,dataArr.length,0);
// 打印结果
console.log(dataArr);
复制代码

执行结果以下,观察执行结果,咱们发现,0号元素所在的树符合最大堆的属性

将乱序数据构建成一个堆

一般状况下,咱们的数据是乱序的,没有规律可言,此时咱们就须要将这些数据构建成堆,heapify实现堆的构建前提是:知道当前操做节点的位置,此时咱们从数据的最后一个节点的父节点出发,进行heapify操做,直至当前操做节点为数组的0号元素时,那么这组数据就成了一个最大堆。

接下来,咱们整理下实现思路:

  • 找到树的最后一个节点
  • 根据数组实现堆中所讲的,寻找父节点位置的公式,根据公式咱们就能够找到当前操做节点的父节点
  • 从树最后一个节点的父节点开始进行heapify操做,直至当前操做节点为0

接下来,咱们将上述思路转化为代码:

/* * 将彻底二叉树构建成堆 * 1. 从树的最后一个父节点开始进行heapify操做 * 2. 树的最后一个父节点 = 树的最后一个子结点的父节点 * */
const buildHeap = function (tree,n) {
    // 最后一个节点的位置 = 数组的长度-1
    const lastNode = n -1;
    // 最后一个节点的父节点
    const parentNode = Math.floor((lastNode - 1) / 2);
    // 从最后一个父节点开始进行heapify操做
    for (let i = parentNode; i  >= 0; i--){
        heapify(tree, n, i);
    }
};
复制代码

接下来咱们测试下buildHeap函数

const dataArr = [23,15,34,11,23,4,19,80];
buildHeap(dataArr,dataArr.length);
console.log(dataArr);
复制代码

观察执行结果,咱们发现数组中的数据已经知足最大堆的属性

实现堆排序

咱们将最大堆构建完成后,根据最大堆的特性可知:堆的顶点为这个堆的最大值,咱们将这个值取出,而后将堆的最后一个节点移动至堆的顶部,而后调用heapify,从新构建堆,直至最大堆中的数据所有被取出则排序完成。

接下来,咱们整理下实现思路:

  • 将数据先构建成一个最大堆
  • 从堆的最后一个节点出发,将其与树的根节点进行位置交换
  • 交换完毕后,调用heapify从新调整堆。
  • 排序好一个数后,咱们的数组长度就会-1,则调用swapheapify时,树的高度就是当前循环到的i的值。

接下来,咱们将上述思路转化为代码:

// 堆排序函数
const heapSort = function (tree,n) {
    // 构建堆
    buildHeap(tree,n);
    // 从最后一个节点出发
    for(let i = n - 1; i >= 0; i--){
        // 交换根节点和最后一个节点的位置
        swap(tree,i,0);
        // 从新调整堆
        heapify(tree,i,0);
    }
};
复制代码

接下来咱们测试下heapSort函数

const dataArr = [23,15,34,11,23,4,19,80];
heapSort(dataArr,dataArr.length);
console.log(dataArr);
复制代码

观察执行结果,咱们发现数组中的数据已经按照从小到大进行排列

写在最后

  • 对堆不了解的开发者,能够阅读个人另外一篇文章:数据结构:堆
  • 文中若有错误,欢迎在评论区指正,若是这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于掘金,未经许可禁止转载💌
相关文章
相关标签/搜索