堆
在学习堆排序之前,首先要了解清楚什么是堆,它可以解决什么,怎么表示。下面我们来看看吧。
什么是堆、大根堆、小根堆
引用维基百科-堆的定义:
堆(英语:Heap)是计算机科学中的一种特别的完全二叉树。若是满足以下特性,即可称为堆:“给定堆中任意节点P和C,若P是C的母节点,那么P的值会小于等于(或大于等于)C的值”。若母节点的值恒小于等于子节点的值,此堆称为最小堆(min heap);反之,若母节点的值恒大于等于子节点的值,此堆称为最大堆(max heap)。在堆中最顶端的那一个节点,称作根节点(root node),根节点本身没有母节点(parent node)。
堆始于J. W. J. Williams在1964年发表的堆排序(heap sort),当时他提出了二叉堆树作为此算法的数据结构。堆在戴克斯特拉算法(英语:Dijkstra's algorithm)中亦为重要的关键。
在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。
简而言之,堆 就是一棵完全二叉树,看下图
而 大(小)根堆 就是在这个二叉树的前提下,加一个条件 —— 在这个树上,所有的子树的最大(小)值都在根顶能取到。看下图:
堆的表示
由上面的堆的解释可以知道,堆是一棵树。但是树这个结构只是我们在思考的时候模拟出来的结构(逻辑结构);而实际上,在计算机上,堆是由数组(物理结构)表示的(当然也可以由链表表示);其父节点和子节点的关系由计算公式表示;下图表示堆的逻辑结构和物理结构的映射:
由上图可知,在数组中,父节点与子节点索引的关系表达式为:
这里 FatherIndex 是以 0 开始的。当然,你也可以以 1 开始,这取决于你的需求。
堆结构适用的场景
举一个例子:
场景:你有一个数组,你可以无限的往里面存放数据;同时你希望每次你取数据的时候,取得的数据是你所有存放的数据的最大值,同时当你取出该数据后,该数据在数组中删除。
此时使用大根堆,堆顶的位置永远是整棵树的最大值。
构建大根堆
构建小根堆与构建大根堆的方法一直,这里以构建大根堆为例。
heapInsert
heapInsert 意味向一个大根堆中插入一个数据,插入后经过调整,仍然保持该堆为大根堆。下面举例说明。
场景:现有大根堆 [8, 4, 3, 4, 1], 向其插入 7; 求插入数据后的大根堆。
因为堆结构本身是数组结构,所以,插入 7 后,未经调整,堆结构示意图如下:
由于原数组本身是大根堆,所以只要调整 7 到合适的位置(7 比父节点小,比所有子孙节点大),就可以保持该堆仍旧为大根堆。**而根据大根堆的定义,7 在调整的时候只需要与自己的父节点进行比较,来判断是否需要上浮;所以调整的次数为该棵树有多少层,即为
经过:7 与 3 进行对比, 7 > 3 上浮; 7 与 8 比较, 7 < 8 停止。最终堆结构如下图
调整 JavaScript 代码如下
/**
* 调整:arr[0 ... index],使得 arr 为 大根堆;
* 前提:arr[0 ... index - 1] 已经是大根堆
* @param {Array<number>} arr 大根堆
* @param {number} index 所需调整的数据的索引
* @returns {void}
*/
function heapInsert(arr, index) {
while (arr[index] > arr[Math.floor((index - 1) / 2)]) {
swap(arr, index, Math.floor((index - 1) / 2));
index = Math.floor((index - 1) / 2);
}
}
/**
* 交换数组中两个变量的位置
* @param {Array<number>} arr 数组
* @param {number} index 所需交换的数据的索引
* @param {number} index2 所需交换的数据的索引
* @returns {void}
*/
function swap(arr, index, index2) {
let temp = arr[index];
arr[index] = arr[index2];
arr[index2] = temp;
}
let arr = [8,4,3,4,1,7];
console.log(arr); //[ 8, 4, 3, 4, 1, 7 ]
heapInsert2(arr, 5);
console.log(arr); //[ 8, 4, 7, 4, 1, 3 ]
基于 heapInsert 构建大根堆
由上面例子可以衍生到调整整个数据成为大根堆。查看下面例子。
场景:将 [1, 2, 3, 4, 5] 调整为大根堆。
思路:由于 heapInsert 函数的功能为:调整:arr[0 ... index],使得 arr 为 大根堆;并且前提是 arr[0 ... index - 1] 已经是大根堆。所以从 0 从小到大依次遍历 [1, 2, 3, 4, 5], 对当前索引值进行 heapInsert,保证 0 到当前索引值这一段数据为大根堆结构。示意图如下。
代码如下
/**
* 堆排序,调整为大根堆
* @param {Array<number>} arr 需要排序的数组
* @returns {void}
*/
function heapSort(arr) {
for(let i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
}
/**
* 调整 arr[index],使得 arr 为 大根堆; 前提:arr[0 ... index - 1] 已经是大根堆
* @param {Array<number>} arr 大根堆
* @param {number} index 所需调整的数据的索引
* @returns {void}
*/
function heapInsert(arr, index) {
while (arr[index] > arr[Math.floor((index - 1) / 2)]) {
swap(arr, index, Math.floor((index - 1) / 2));
index = Math.floor((index - 1) / 2);
}
}
/**
* 交换数组中两个变量的位置
* @param {Array<number>} arr 数组
* @param {number} index 所需交换的数据的索引
* @param {number} index2 所需交换的数据的索引
* @returns {void}
*/
function swap(arr, index, index2) {
let temp = arr[index];
arr[index] = arr[index2];
arr[index2] = temp;
}
let arr = [1,2,3,4,5];
console.log(arr); //[ 1, 2, 3, 4, 5 ]
heapSort(arr);
console.log(arr); //[ 5, 4, 2, 1, 3 ]
heapInsert 堆排序的时间复杂度
由流程可知,假设数组长度为 n,则一共需要进行 n 次 heapInsert;而一次 heapInsert 的调整次数为 ; 则heapInsert 堆排序的时间复杂度为 ;即
heapify
heapInsert 是一种由上至下的调整堆的思想。如果我们需要调整一个给定的数据,也可以使用自下而上的调整方式 —— heapify;
同样使用 [1, 2, 3, 4, 5] 为例
思路:从数组最后一个数进行遍历,索引值依次减少。保证以当前索引值为堆顶的堆为大根堆。示意图如下。
/**
* 堆排序,调整为大根堆
* @param {Array<number>} arr 需要排序的数组
* @returns {void}
*/
function heapSort(arr) {
for (let i = arr.length - 1; i >= 0; i--) {
heapify(arr, i, arr.length - 1);
}
}
/**
* 调整 arr[index],使得 arr 为 大根堆;
* @param {Array<number>} arr 大根堆
* @param {number} start 所需调整的数据的索引
* @param {number} end arr最后一个数据的索引值
* @returns {void}
*/
function heapify(arr, start, end) {
if (start * 2 + 1 > end) return;
let largestChildIndex = start * 2 + 2 <= end ? arr[start * 2 + 1] > arr[start * 2 + 2] ? start * 2 + 1 : start * 2 + 2 : start * 2 + 1;
while (largestChildIndex <= end) {
if (arr[start] < arr[largestChildIndex]) {
swap(arr, start, largestChildIndex);
}
start = largestChildIndex;
largestChildIndex = start * 2 + 2 <= end ? arr[start * 2 + 1] > arr[start * 2 + 2] ? start * 2 + 1 : start * 2 + 2 : start * 2 + 1;
}
}
/**
* 交换数组中两个变量的位置
* @param {Array<number>} arr 数组
* @param {number} index 所需交换的数据的索引
* @param {number} index2 所需交换的数据的索引
* @returns {void}
*/
function swap(arr, index, index2) {
let temp = arr[index];
arr[index] = arr[index2];
arr[index2] = temp;
}
let arr = [1, 2, 3, 4, 5];
console.log(arr); //[ 1, 2, 3, 4, 5 ]
heapSort(arr);
console.log(arr); //[ 5, 4, 3, 1, 2 ]
堆排序的稳定性
堆排序是不稳定的算法。举例:[1、5、2、3、2、6、2],看下图可知,三个2的先后顺序经过排序后改变了
弹出最大值
将一个大根堆的最大值弹出如何处理呢?由于是大根堆,所以最大值一定是在堆顶,即索引值为 0;将 索引值为 0 索引值为 MaxIndex(该数组的最大索引值) 交换。针对 0 ... MaxIndex - 1 进行重新排序即可。