排序算法-3(javascript) 堆排序的实现

前面我们已经讲过二叉堆是啥了,然后也晓得最大堆和最小堆的实现。(不晓得的同学,传送门走起://www.greatytc.com/p/d79e39430cd2

一般数组排序,我们都是默认按从小到大排,所以这要用到的最大堆(如果是按从大到小,那么同理就用最小堆实现)。所以后面说到堆都是特指最大堆哈。

先看下最大堆的实现:

/*
  二叉堆的最大堆代码实现
*/

// 根据子节点下标的奇偶性,来获取对应父节点的下标
function getParentIndex(childIndex) {
  let parentIndex = 0;
  if(childIndex % 2 === 0) {
    parentIndex = (childIndex - 2) / 2  // 如果是右节点,父节点的下标
  }else{
    parentIndex = (childIndex - 1) / 2  // 如果是左节点,父节点的下标
  }
  return parentIndex;
}


// 最大堆上浮(一般对应插入操作)
function upAdjust(arr) {
  let childIndex = arr.length -1; // 初始子节点的下标
  parentIndex = getParentIndex(childIndex)
  let temp = arr[childIndex]  // 子节点值
  while((childIndex > 0) && (temp > arr[parentIndex])) {
    arr[childIndex] = arr[parentIndex]
    childIndex = parentIndex
    // 根据子节点下标的奇偶性,来获取对应父节点的下标
    parentIndex = getParentIndex(childIndex)
  }
  arr[childIndex] = temp
  return arr;
}

const testArr1 = upAdjust([4,2,9,1,7,8,36])
console.log(testArr1); // [ 36, 2, 4, 1, 7, 8, 9 ]


/**
 * @description 二叉堆下沉(最大堆)
 * @param arr: 待调整的二叉堆
 * @param parentIndex: 要下沉的父节点位置
 * @param length: 堆的有效长度,也就是要做下沉的区间范围
 * @return arr: 调整后的二叉堆数组
 */
function downAdjust(arr, parentIndex, length) {
  // const len = arr.length;
  // 保存父节点的值,用于最后赋值
  let temp = arr[parentIndex]
  // 左节点下标值
  let childIndex = parentIndex * 2 + 1;
  // 如果存在左孩子节点
  while(childIndex < length) {
    // 检测是否存在右节点,且右节点比左节点大点,则取右节点下标值;否则还是使用左节点下标
    if(childIndex + 1 < length && arr[childIndex + 1] > arr[childIndex]) {
      childIndex++;
    }
    // 如果父节点值比左右节点中最大的节点值都要大,那么结束
    if(temp > arr[childIndex]) break;
    // 否则,将爷节点与最大子节点值交换
    arr[parentIndex] = arr[childIndex]
    parentIndex = childIndex
    childIndex = parentIndex * 2 + 1
  }
  arr[parentIndex] = temp
  return arr;
}

const testArr2 = downAdjust([4,2,9,36,7,8,1,3], 0, 8)
console.log(testArr2); // [ 9, 2, 8, 36, 7, 4, 1, 3 ]


/**
 * @description 构造二叉堆,将无序的完全二叉树转换成一个二叉堆(最小堆)
 * @param arr: 待调整的完全二叉树数组
 * @return arr: 调整后的二叉堆
 */
function buildMaxHeap(arr) {
  let childIndex = arr.length - 1
  let parentInddex = getParentIndex(childIndex)
  for(let i = parentInddex; i >= 0; i--) {
    downAdjust(arr, i, arr.length)
  }
  return arr;
}
const testArr3 = buildMaxHeap([4,2,9,36,7,8,1,3])
console.log("testArr3:", testArr3); // [ 36, 7, 9, 3, 4, 8, 1, 2 ]

堆的下沉操就是把要下沉的元素(后面记做parent了),依次对比它的左右子节点,记录子节点中最大值 childMax = max(左子节点,右子节点),如果子节点max都比parent小,则结束;如果childMax > parent,则两者交换位置。然后将这个parent继续做下沉操作,直至子节点中没有比parent大的值或没有子节点为止。

其实堆下沉,我们往往是用来用删除操作。
比如:这个最大堆[ 36, 7, 9, 3, 4, 8, 1, 2 ],我们如果要删除36,就是把2和36的位置交换,然后删除36,然后对2进行下沉,最后得到[ 9, 7, 8, 3, 4, 2, 1 ],会发现9是下个堆的最大值,也就是原堆的第二大值。

利用这个特性,我们每次将删除的堆顶,都不做删除操作(假删除操作),而是丢到除了有序区间的最后一位。我们就可以简单地实现堆排序了:

  1. 初始堆[ 36, 7, 9, 3, 4, 8, 1, 2 ],第1次执行删除36,最后一位与36交换得到[ 2, 7, 9, 3, 4, 8, 1, 36 ],然后对2进行下沉操作时,忽略最后一个节点,这样就可以得到[ 9, 7, 8, 3, 4, 2, 1, 36 ],执行第1次后就可以把最大值放在最后了
  2. 第2次执行删除9,和倒数第二位1换位置,[ 1, 7, 8, 3, 4, 2, 9, 36 ],然后对1进行下沉操作,忽略后二位节点,这样就可以得到[ 8, 7, 2, 3, 4, 1, 9, 36 ],执行第2次后就可以得到最后两位的有序区间了
  3. 第3次执行删除8,和倒数第三位1交换位置[ 1, 7, 2, 3, 4, 8, 9, 36 ],然后对1进行下沉操作,忽略后三位节点,这样就可以得到[ 7, 4, 2, 3, 1, 8, 9, 36 ]
  4. 第4次执行删除7,和倒数第4位交换位置[ 1, 4, 2, 3, 7, 8, 9, 36 ],然后对1进行下沉操作,忽略后4四位有序区间,这样就可以得到[ 4, 3, 2, 1, 7, 8, 9, 36 ]
  5. 第5次执行删除4,和倒数第5位交换位置[ 1, 3, 2, 4, 7, 8, 9, 36 ],然后对1进行下沉操作,忽略后5四位有序区间,这样就可以得到[ 3, 1, 2, 4, 7, 8, 9, 36 ]
  6. 第6次执行删除3,和倒数第6位交换位置[ 2, 1, 3, 4, 7, 8, 9, 36 ],然后对2进行下沉操作,忽略后6四位有序区间,这样就可以得到[ 1, 2, 3, 4, 7, 8, 9, 36 ],这时就剩下1个1是堆顶了,没有要比较的队列了,因为右边的已经全是有序队列了,所以此时结束后,就能得到排序结果了

所以只要在最大堆的基础上,做次遍历删除操作就好了:

/*
*  堆排序实现
*/
function heapSort(arr) {
  // 将无序数组转换成为最大堆
  let heap = buildMaxHeap(arr)

  // 然后依次对堆进行删除操作
  for(let i = heap.length -1; i > 0; i--) {
    // 每次将堆顶假删除,放最后
    [ heap[0], heap[i] ] = [ heap[i], heap[0] ]
    // 然后对新的堆顶做下沉操作,构成新的最大堆
    downAdjust(heap, 0, i)
  }
  return heap;
}

let heap = heapSort([7, 36, 9, 3, 4, 8, 1, 2])
console.log('heap:', heap); // [ 1, 2, 3, 4, 7, 8, 9, 36 ]

这时候,我们来分析下它的时间复杂度和空间复杂度了:

  • 空间复杂度,基本上没有创建新地址空间,所以是O(1)
  • 时间复杂度:
    1. 把无序数组转换成堆的复杂度是O(n)
    2. 下沉操作是O(logn),遍历操作是O(n)
      所以总的是O(n + nlogn),相当于O(n(1+logn)),然后常量可以忽略,所以平均下来就约等于O(nlogn)了

排序算法系列文章传送门(未完,持续更新中):
排序算法-1(javascript) 冒泡、选择、插入、希尔排序的实现
排序算法-2(javascript) 快速排序的实现
排序算法-3(javascript) 堆排序的实现
排序算法-4(javascript) 归并排序的实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容