图文详解Heap Sort堆排序算法及JavaScript的代码实现


Posted in Javascript onMay 04, 2016

1. 不得不说说二叉树
要了解堆首先得了解一下二叉树,在计算机科学中,二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。
二叉树的每个结点至多只有二棵子树(不存在度大于 2 的结点),二叉树的子树有左右之分,次序不能颠倒。二叉树的第 i 层至多有 2i - 1 个结点;深度为 k 的二叉树至多有 2k - 1 个结点;对任何一棵二叉树 T,如果其终端结点数为 n0,度为 2 的结点数为 n2,则n0 = n2 + 1。
树和二叉树的三个主要差别:
树的结点个数至少为 1,而二叉树的结点个数可以为 0
树中结点的最大度数没有限制,而二叉树结点的最大度数为 2
树的结点无左、右之分,而二叉树的结点有左、右之分
二叉树又分为完全二叉树(complete binary tree)和满二叉树(full binary tree)
满二叉树:一棵深度为 k,且有 2k - 1 个节点称之为满二叉树

图文详解Heap Sort堆排序算法及JavaScript的代码实现

(深度为 3 的满二叉树 full binary tree)
完全二叉树:深度为 k,有 n 个节点的二叉树,当且仅当其每一个节点都与深度为 k 的满二叉树中序号为 1 至 n 的节点对应时,称之为完全二叉树

图文详解Heap Sort堆排序算法及JavaScript的代码实现

(深度为 3 的完全二叉树 complete binary tree)
2. 什么是堆?
堆(二叉堆)可以视为一棵完全的二叉树,完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
如下图,是一个堆和数组的相互关系

图文详解Heap Sort堆排序算法及JavaScript的代码实现

(堆和数组的相互关系)
对于给定的某个结点的下标 i,可以很容易的计算出这个结点的父结点、孩子结点的下标:
Parent(i) = floor(i/2),i 的父节点下标
Left(i) = 2i,i 的左子节点下标
Right(i) = 2i + 1,i 的右子节点下标

图文详解Heap Sort堆排序算法及JavaScript的代码实现

二叉堆一般分为两种:最大堆和最小堆。
最大堆:
最大堆中的最大元素值出现在根结点(堆顶)
堆中每个父节点的元素值都大于等于其孩子结点(如果存在)

图文详解Heap Sort堆排序算法及JavaScript的代码实现

(最大堆)
最小堆:
最小堆中的最小元素值出现在根结点(堆顶)
堆中每个父节点的元素值都小于等于其孩子结点(如果存在)

图文详解Heap Sort堆排序算法及JavaScript的代码实现

(最小堆)
3. 堆排序原理
堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。在堆中定义以下几种操作:
最大堆调整(Max-Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build-Max-Heap):将堆所有数据重新排序,使其成为最大堆
堆排序(Heap-Sort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
继续进行下面的讨论前,需要注意的一个问题是:数组都是 Zero-Based,这就意味着我们的堆数据结构模型要发生改变

图文详解Heap Sort堆排序算法及JavaScript的代码实现

(Zero-Based)
相应的,几个计算公式也要作出相应调整:
Parent(i) = floor((i-1)/2),i 的父节点下标
Left(i) = 2i + 1,i 的左子节点下标
Right(i) = 2(i + 1),i 的右子节点下标
最大堆调整(MAX?HEAPIFY)的作用是保持最大堆的性质,是创建最大堆的核心子程序,作用过程如图所示:

图文详解Heap Sort堆排序算法及JavaScript的代码实现

(Max-Heapify)
由于一次调整后,堆仍然违反堆性质,所以需要递归的测试,使得整个堆都满足堆性质,用 JavaScript 可以表示如下:

/**
 * 从 index 开始检查并保持最大堆性质
 *
 * @array
 *
 * @index 检查的起始下标
 *
 * @heapSize 堆大小
 *
 **/
function maxHeapify(array, index, heapSize) {
 var iMax = index,
   iLeft = 2 * index + 1,
   iRight = 2 * (index + 1);

 if (iLeft < heapSize && array[index] < array[iLeft]) {
  iMax = iLeft;
 }

 if (iRight < heapSize && array[iMax] < array[iRight]) {
  iMax = iRight;
 }

 if (iMax != index) {
  swap(array, iMax, index);
  maxHeapify(array, iMax, heapSize); // 递归调整
 }
}

function swap(array, i, j) {
 var temp = array[i];
 array[i] = array[j];
 array[j] = temp;
}

通常来说,递归主要用在分治法中,而这里并不需要分治。而且递归调用需要压栈/清栈,和迭代相比,性能上有略微的劣势。当然,按照20/80法则,这是可以忽略的。但是如果你觉得用递归会让自己心里过不去的话,也可以用迭代,比如下面这样:

/**
 * 从 index 开始检查并保持最大堆性质
 *
 * @array
 *
 * @index 检查的起始下标
 *
 * @heapSize 堆大小
 *
 **/
function maxHeapify(array, index, heapSize) {
 var iMax, iLeft, iRight;
 while (true) {
  iMax = index;
  iLeft = 2 * index + 1;
  iRight = 2 * (index + 1);
  if (iLeft < heapSize && array[index] < array[iLeft]) {
   iMax = iLeft;
  }

  if (iRight < heapSize && array[iMax] < array[iRight]) {
   iMax = iRight;
  }

  if (iMax != index) {
   swap(array, iMax, index);
   index = iMax;
  } else {
   break;
  }
 }
}

function swap(array, i, j) {
 var temp = array[i];
 array[i] = array[j];
 array[j] = temp;
}

创建最大堆(Build-Max-Heap)的作用是将一个数组改造成一个最大堆,接受数组和堆大小两个参数,Build-Max-Heap 将自下而上的调用 Max-Heapify 来改造数组,建立最大堆。因为 Max-Heapify 能够保证下标 i 的结点之后结点都满足最大堆的性质,所以自下而上的调用 Max-Heapify 能够在改造过程中保持这一性质。如果最大堆的数量元素是 n,那么 Build-Max-Heap 从 Parent(n) 开始,往上依次调用 Max-Heapify。流程如下:

图文详解Heap Sort堆排序算法及JavaScript的代码实现

用 JavaScript 描述如下:

function buildMaxHeap(array, heapSize) {
 var i,
   iParent = Math.floor((heapSize - 1) / 2);
   
 for (i = iParent; i >= 0; i--) {
  maxHeapify(array, i, heapSize);
 }
}

堆排序(Heap-Sort)是堆排序的接口算法,Heap-Sort先调用Build-Max-Heap将数组改造为最大堆,然后将堆顶和堆底元素交换,之后将底部上升,最后重新调用Max-Heapify保持最大堆性质。由于堆顶元素必然是堆中最大的元素,所以一次操作之后,堆中存在的最大元素被分离出堆,重复n-1次之后,数组排列完毕。整个流程如下:

图文详解Heap Sort堆排序算法及JavaScript的代码实现

用 JavaScript 描述如下:

function heapSort(array, heapSize) {

 buildMaxHeap(array, heapSize);

 for (int i = heapSize - 1; i > 0; i--) {
  swap(array, 0, i);
  maxHeapify(array, 0, i);
 } 
}

4.JavaScript 语言实现
最后,把上面的整理为完整的 javascript 代码如下:

function heapSort(array) {

 function swap(array, i, j) {
  var temp = array[i];
  array[i] = array[j];
  array[j] = temp;
 }

 function maxHeapify(array, index, heapSize) {
  var iMax,
   iLeft,
   iRight;
  while (true) {
   iMax = index;
   iLeft = 2 * index + 1;
   iRight = 2 * (index + 1);

   if (iLeft < heapSize && array[index] < array[iLeft]) {
    iMax = iLeft;
   }

   if (iRight < heapSize && array[iMax] < array[iRight]) {
    iMax = iRight;
   }

   if (iMax != index) {
    swap(array, iMax, index);
    index = iMax;
   } else {
    break;
   }
  }
 }

 function buildMaxHeap(array) {
  var i,
   iParent = Math.floor(array.length / 2) - 1;

  for (i = iParent; i >= 0; i--) {
   maxHeapify(array, i, array.length);
  }
 }

 function sort(array) {
  buildMaxHeap(array);

  for (var i = array.length - 1; i > 0; i--) {
   swap(array, 0, i);
   maxHeapify(array, 0, i);
  }
  return array;
 }

 return sort(array);
}

5.堆排序算法的运用

(1)算法性能/复杂度
堆排序的时间复杂度非常稳定(我们可以看到,对输入数据不敏感),为O(n?n)复杂度,最好情况与最坏情况一样。
但是,其空间复杂度依实现不同而不同。上面即讨论了两种常见的复杂度:O(n)与O(1)。本着节约空间的原则,我推荐O(1)复杂度的方法。

(2)算法稳定性
堆排序存在大量的筛选和移动过程,属于不稳定的排序算法。

(3)算法适用场景
堆排序在建立堆和调整堆的过程中会产生比较大的开销,在元素少的时候并不适用。但是,在元素比较多的情况下,还是不错的一个选择。尤其是在解决诸如“前n大的数”一类问题时,几乎是首选算法。

Javascript 相关文章推荐
滚动经典最新话题[prototype框架]下编写
Oct 03 Javascript
理解Javascript_09_Function与Object
Oct 16 Javascript
JavaScript限定复选框的选择个数示例代码
Aug 25 Javascript
jquery.mobile 共同布局遇到的问题小结
Feb 10 Javascript
javascript实现英文首字母大写
Apr 23 Javascript
基于JavaScript实现滑动门效果
Mar 16 Javascript
JS异步加载的三种实现方式
Mar 16 Javascript
如何让你的JS代码更好看易读
Dec 01 Javascript
Vue中插入HTML代码的方法
Sep 21 Javascript
微信小程序dom操作的替代思路实例分析
Dec 06 Javascript
实例讲解JS中pop使用方法
Jan 27 Javascript
微信小程序 多行文本显示...+显示更多按钮和收起更多按钮功能
Sep 26 Javascript
一分钟理解js闭包
May 04 #Javascript
学JavaScript七大注意事项【必看】
May 04 #Javascript
Jquery ui datepicker设置日期范围,如只能隔3天【实现代码】
May 04 #Javascript
开启BootStrap学习之旅
May 04 #Javascript
JavaScript入门教程之引用类型
May 04 #Javascript
javascript和jquery实现用户登录验证
May 04 #Javascript
基于Bootstrap使用jQuery实现简单可编辑表格
May 04 #Javascript
You might like
php+dbfile开发小型留言本
2006/10/09 PHP
PHP 分页原理分析,大家可以看看
2009/12/21 PHP
php mysql_real_escape_string函数用法与实例教程
2013/09/30 PHP
Yii CGridView用法实例详解
2016/07/12 PHP
laravel通过a标签从视图向控制器实现传值
2019/10/15 PHP
PHP实现简单注册登录系统
2020/12/28 PHP
TNC vs IO BO3 第一场2.13
2021/03/10 DOTA
javascript游戏开发之《三国志曹操传》零部件开发(三)情景对话中仿打字机输出文字
2013/01/23 Javascript
jquery在项目中做复选框时遇到的一些问题笔记
2013/11/17 Javascript
jquery遍历之parent()和parents()的区别及parentsUntil()方法详解
2013/12/02 Javascript
jquery 自定义容器下雨效果可将下雨图标改为其他
2014/04/23 Javascript
protractor的安装与基本使用教程
2017/07/07 Javascript
解决vue多个路由共用一个页面的问题
2018/03/12 Javascript
layDate插件设置开始和结束时间
2018/11/15 Javascript
微信小程序websocket实现即时聊天功能
2019/05/21 Javascript
详解一次Vue低版本安卓白屏问题的解决过程
2019/05/30 Javascript
javascript实现的图片预览和上传功能示例【兼容IE 9】
2020/05/01 Javascript
浅谈实现在线预览PDF的几种解决办法
2020/08/10 Javascript
关于vue 项目中浏览器跨域的配置问题
2020/11/10 Javascript
python中合并两个文本文件并按照姓名首字母排序的例子
2014/04/25 Python
使用Python脚本将Bing的每日图片作为桌面的教程
2015/05/04 Python
利用Python找出序列中出现最多的元素示例代码
2017/12/08 Python
基于Python计算圆周率pi代码实例
2020/03/25 Python
django 将自带的数据库sqlite3改成mysql实例
2020/07/09 Python
Python Pillow(PIL)库的用法详解
2020/09/19 Python
PyCharm常用配置和常用插件(小结)
2021/02/06 Python
CSS3之边框多颜色Border-color属性使用示例
2013/10/11 HTML / CSS
HTML5各种头部meta标签的功能(推荐)
2017/03/13 HTML / CSS
Kneipp克奈圃美国官网:德国百年精油配方的传承
2018/02/07 全球购物
Araks官网:纽约内衣品牌
2020/10/15 全球购物
学生个人自我鉴定
2014/03/26 职场文书
大学竞选班长演讲稿
2014/04/24 职场文书
学生会竞选演讲稿学习部
2014/08/25 职场文书
2014年校务公开工作总结
2014/12/18 职场文书
乡镇科协工作总结2015
2015/05/19 职场文书
毕业季聚会祝酒词!
2019/07/04 职场文书