掘金团队号上线,助你 Offer 临门! 点击查看详情
什么是堆排序
堆排序就是利用二叉堆数据结构的性质(最大堆或者最小堆的根节点都存放着当前堆的最大或最小元素),对一组数据进行排序的算法。堆排序和快速排序的平均时间复杂度相同,并且都是不稳定排序,堆排序不适合数据量较少的情况,因为光构建堆就要进行很多次比较。
实现步骤
实现堆排序,含有以下三个步骤:
- 将无序数组整理为堆,具体步骤为找到无序数组最后一个非叶子节点(也是最后一个父节点),随后对每一个非叶子节点都进行一次堆整理(下移操作),直到所有节点以下的子堆都已符合最小堆/最大堆。
-
将无序数组整理为最大堆,堆排序后得到一个升序排列的数组(从最小到最大)
-
将无序数组整理为最小堆,堆排序后得到一个降序排列的数组(从最大到最小)
步骤一可视化视频:
-
将堆的尾节点与堆的根节点交换,并将堆的大小往前挪一个单位(等同于移除位于原来位于堆的根节点),并从根节点开始,做最大堆/最小堆的堆调整。
步骤二可视化视频:
-
最后返回排序后的堆
创建用于堆排序的方法heapSort
// 比较用的常量对象(保证代码优雅)
export const Compare = {
LESS_THAN: -1, // 如果第一个元素小于第二个元素,它就返回-1
BIGGER_THAN: 1, // 如果第一个元素大于第二个元素,它就返回1
EQUALS: 0, // 如果元素有相同的引用,它就返回 0
};
// 反转后的比较方法(用于最大堆)
export function reverseCompare(compareFn) {
return (a, b) => compareFn(b, a);
}
// 比较用的方法
export function defaultCompare(a, b) {
// 如果元素有相同的引用,它就返回 0
if (a === b) {
return Compare.EQUALS;
}
// 如果第一个元素小于第二个元素,它就返回-1,否之返回1
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
/**
* 交换函数
* @param {*} array 传入需要交换的数组(这里传入堆)
* @param {*} a 传入要交换的节点A
* @param {*} b 传入要交换的节点B
*/
export function swap(array, a, b) {
// ES5写法(性能较好)
/* const temp = array[a]; // 要交换数组中的两个值,我们需要一个辅助变量来复制要交换的第一个元素
array[a] = array[b]; // 然后,将第二个元素赋值到第一个元素的位置
array[b] = temp; */ // 最后,将复制的第一个元素的值覆盖到第二个元素的位置
// ES6写法(性能较差) https://bugzilla.mozilla.org/show_bug.cgi?id=1177319
[array[a], array[b]] = [array[b], array[a]];
}
/**
* 堆排序
* @param {string} sort 排序类型,sort值可以是asc或desc,asc则为升序(使用最大堆),desc则为降序(使用最小堆),默认asc升序排序
* @param {array} array 需要排序的数组
* @param {function} compareFn 比较用的函数,默认比较函数是 defaultCompare
* @returns {array} 返回排序后的数组
*/
export default function heapSort(
sort = "asc",
array,
compareFn = defaultCompare
) {
// 校验排序类型的合法性
if (sort !== "asc" || sort !== "desc") {
return undefined;
}
// 如果排序方式为降序,需要反转比较函数
if (sort === "desc") {
compareFn = reverseCompare(compareFn);
}
// 将待排序的数组长度保存到临时变量heapSize
let heapSize = array.length;
// 将待排序数组转换为最大堆或最小堆(视排序方式而异)
buildHeap(array, compareFn);
// 排序
// 执行while循环,循环条件是heap大小大于1
while (heapSize > 1) {
swap(array, 0, --heapSize); // 将堆的尾节点与堆的根节点交换,并且减少堆的长度(减少长度之后的堆节点已排序好)
heapify(array, 0, heapSize, compareFn); // 从根节点开始调整堆,当前堆的尾节点已经是最大值(减去的堆长度),所以这里调整堆的长度是已经减少之后的长度
}
// 返回排序后的堆
return array;
}
将无序数组调整为堆
/**
* 将无序数组调整为堆
* @param {array} array 待转换为堆的数组
* @param {function} compareFn 比较用的函数,默认比较函数是 defaultCompare
* @returns {array} 返回生成后的堆
*/
export function buildHeap(array, compareFn) {
// 把无序数组构建成二叉堆,需要进行n/2次循环,每次循环调用一次 heapify 方法,所以第一步的计算规模是 n/2 * logn,时间复杂度 O(nlogn)。
// 使用 n-2次循环,便于快速寻找最后一个非叶子节点(最后一个父节点)
// 随后对每一个非叶子节点都执行一次heapify 操作
// 这样到最后,就满足所有结点以下的子堆都已经符合最小堆或最大堆
for (let i = Math.floor(array.length / 2); i >= 0; i -= 1) {
heapify(array, i, array.length, compareFn);
}
return array;
}
堆调整(下移操作)
/**
* 堆调整---同下移操作(堆化)
* 下移操作表示将元素和最小子节点(最小堆)和最大子节点(最大堆)进行交换
* @param {number} array 传入需要下移操作的数组
* @param {number} index 需要调整的元素位置(index)
* @param {number} heapSize 传入堆的大小
* @param {number} compareFn 需要使用的比较函数
*/
function heapify(array, index, heapSize, compareFn) {
let largest = index; // 声明一个变量(largest)保存index
const left = 2 * index + 1; // 获取index的左测子节点(left)
const right = 2 * index + 2; // 获取index的右侧子节点(right)
if (
left < heapSize &&
this.compareFn(array[left], array[largest]) === Compare.BIGGER_THAN
) { // 如果array[largest] > array[left],则更新largest的值为left(left < heapSize 用于校验index是否合法)
largest = left; // 交换元素和它的左侧子节点
}
if (
right < heapSize &&
this.compareFn(array[right], array[largest]) === Compare.BIGGER_THAN
) { // 如果array[largest] > array[right],则更新element的值为right(left < heapSize 用于校验index是否合法)
largest = right; // 交换元素和它的右侧子节点
}
// 交换完后判断当前保存的element和index的值是否相同,如果不相同,则未找到最小堆的位置
if (index !== largest) {
// 如果不相同,则还未找到最小子节点的位置
swap(array, index, largest); // 我们将这个元素和左/右侧子节点交换(交换index和largest位置的元素)
heapify(array, largest, heapSize, compareFn); // 重复这个过程(继续执行heapify函数)
}
}
完整代码
// 比较用的常量对象(保证代码优雅)
export const Compare = {
LESS_THAN: -1, // 如果第一个元素小于第二个元素,它就返回-1
BIGGER_THAN: 1, // 如果第一个元素大于第二个元素,它就返回1
EQUALS: 0, // 如果元素有相同的引用,它就返回 0
};
// 反转后的比较方法(用于最大堆)
export function reverseCompare(compareFn) {
return (a, b) => compareFn(b, a);
}
// 比较用的方法
export function defaultCompare(a, b) {
// 如果元素有相同的引用,它就返回 0
if (a === b) {
return Compare.EQUALS;
}
// 如果第一个元素小于第二个元素,它就返回-1,否之返回1
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
/**
* 交换函数
* @param {*} array 传入需要交换的数组(这里传入堆)
* @param {*} a 传入要交换的节点A
* @param {*} b 传入要交换的节点B
*/
export function swap(array, a, b) {
// ES5写法(性能较好)
/* const temp = array[a]; // 要交换数组中的两个值,我们需要一个辅助变量来复制要交换的第一个元素
array[a] = array[b]; // 然后,将第二个元素赋值到第一个元素的位置
array[b] = temp; */ // 最后,将复制的第一个元素的值覆盖到第二个元素的位置
// ES6写法(性能较差) https://bugzilla.mozilla.org/show_bug.cgi?id=1177319
[array[a], array[b]] = [array[b], array[a]];
}
/**
* 堆调整---同下移操作(堆化)
* 下移操作表示将元素和最小子节点(最小堆)和最大子节点(最大堆)进行交换
* @param {number} array 传入需要下移操作的数组
* @param {number} index 需要调整的元素位置(index)
* @param {number} heapSize 传入堆的大小
* @param {number} compareFn 需要使用的比较函数
*/
function heapify(array, index, heapSize, compareFn) {
let largest = index; // 声明一个变量(largest)保存index
const left = 2 * index + 1; // 获取index的左测子节点(left)
const right = 2 * index + 2; // 获取index的右侧子节点(right)
if (
left < heapSize &&
this.compareFn(array[left], array[largest]) === Compare.BIGGER_THAN
) { // 如果array[largest] > array[left],则更新largest的值为left(left < heapSize 用于校验index是否合法)
largest = left; // 交换元素和它的左侧子节点
}
if (
right < heapSize &&
this.compareFn(array[right], array[largest]) === Compare.BIGGER_THAN
) { // 如果array[largest] > array[right],则更新element的值为right(left < heapSize 用于校验index是否合法)
largest = right; // 交换元素和它的右侧子节点
}
// 交换完后判断当前保存的element和index的值是否相同,如果不相同,则未找到最小堆的位置
if (index !== largest) {
// 如果不相同,则还未找到最小子节点的位置
swap(array, index, largest); // 我们将这个元素和左/右侧子节点交换(交换index和largest位置的元素)
heapify(array, largest, heapSize, compareFn); // 重复这个过程(继续执行heapify函数)
}
}
/**
* 将无序数组调整为堆
* @param {array} array 待转换为堆的数组
* @param {function} compareFn 比较用的函数,默认比较函数是 defaultCompare
* @returns {array} 返回生成后的堆
*/
export function buildHeap(array, compareFn) {
// 把无序数组构建成二叉堆,需要进行n/2次循环,每次循环调用一次 heapify 方法,所以第一步的计算规模是 n/2 * logn,时间复杂度 O(nlogn)。
// 使用 n-2次循环,便于快速寻找最后一个非叶子节点(最后一个父节点)
// 随后对每一个非叶子节点都执行一次heapify 操作
// 这样到最后,就满足所有结点以下的子堆都已经符合最小堆或最大堆
for (let i = Math.floor(array.length / 2); i >= 0; i -= 1) {
heapify(array, i, array.length, compareFn);
}
return array;
}
/**
* 堆排序
* @param {string} sort 排序类型,sort值可以是asc或desc,asc则为升序(使用最大堆),desc则为降序(使用最小堆),默认asc升序排序
* @param {array} array 需要排序的数组
* @param {function} compareFn 比较用的函数,默认比较函数是 defaultCompare
* @returns {array} 返回排序后的数组
*/
export default function heapSort(
sort = "asc",
array,
compareFn = defaultCompare
) {
// 校验排序类型的合法性
if (sort !== "asc" || sort !== "desc") {
return undefined;
}
// 如果排序方式为降序,需要反转比较函数
if (sort === "desc") {
compareFn = reverseCompare(compareFn);
}
// 将待排序的数组长度保存到临时变量heapSize
let heapSize = array.length;
// 将待排序数组转换为最大堆或最小堆(视排序方式而异)
buildHeap(array, compareFn);
// 排序
// 执行while循环,循环条件是heap大小大于1
while (heapSize > 1) {
swap(array, 0, --heapSize); // 将堆的尾节点与堆的根节点交换,并且减少堆的长度(减少长度之后的堆节点已排序好)
heapify(array, 0, heapSize, compareFn); // 从根节点开始调整堆,当前堆的尾节点已经是最大值(减去的堆长度),所以这里调整堆的长度是已经减少之后的长度
}
// 返回排序后的堆
return array;
}
完整堆排序演示
完整堆排序演示可视化视频:
堆排序的可视化体验:
堆排序与快速排序的比较
- 堆排序和快速排序的平均时间复杂度都是O(nlogn),并且都是不稳定排序。
- 快速排序的最坏时间复杂度是O(n^2),而堆排序最坏时间复杂度稳定在O(nlogn)。
- 快速排序的递归和非递归方法空间复杂度都是O(n),而堆排序的空间复杂度是O(1)。
- 堆排序不适合数据量较少的情况,因为光构建堆就要进行很多次比较