什么是堆?
在学习堆排序前先温习下完全二叉树的概念:
一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。
完全二叉树具有如下性质:
将根节点的下标视为0 ,则 第i个数 的左子节点的下标为2i+1,右子节点的下标为2i+2;
对于有n个元素的完全二叉树(n>=2),它的最后一个非叶子结点的下标为n/2 - 1.
堆常常在两种场景下出现:
在程序内存布局场景中,堆(heap)和栈(stack)代表两种内存管理方式,其中堆有程序员使用内存分配函数malloc分配和free释放;而栈是由系统自动分配和释放,无需人工控制,例如在一个函数中申明的变量,就存在栈内存中,由系统来管理。
在数据类型场景中,堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看作一棵完全二叉树的数组对象。堆总是满足下列性质:
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一棵完全二叉树。
根节点的值大于等于子节点的值,这样的堆被称为最大堆,或大顶堆;
根节点的值小于等于子节点的值,这样的堆被称为最小堆,或小顶堆 。常见的堆有二叉堆、斐波那契堆等。
堆的物理结构本质上是顺序存储的,是线性的。但在逻辑上不是线性的,是完全二叉树的这种逻辑储存结构。 堆的这个数据结构,里面的成员包括一维数组,数组的容量,数组元素的个数,有两个直接后继。
堆的定义:n个元素的序列当且仅当满足(
且
)或(
且
)时,称之为堆。
如何把一个满二叉树看做堆?
如图,对于 0号元素,位于顶,1号元素为左子树根结点,2号元素为右子树根节点,3号元素为1号元素的左子树根节点/叶子,4号元素为1号元素的右子树根节点/叶子。 以此类推...
堆排序思想
用数列构建出一个大顶堆,去除堆顶的数字;
调整剩余的数字,构建出新的大顶堆,再次去除堆顶的数字;
循环往复,完成整个排序。
堆排序过程:
- 用数列构建出一个大顶堆,取出堆顶的数字;
- 调整剩余的数字,构建出新的大顶堆,再次取出堆顶的数字;
- 循环往复,完成整个排序。
构建大顶堆的两种方式
方案一:从 0 开始,将每个数字依次插入堆中,一边插入,一边调整堆的结构,使其满足大顶堆的要求;
方案二:将整个数列的初始状态视作一棵完全二叉树,自底向上调整树的结构,使其满足大顶堆的要求。
方案一可使用堆定义进行构建:n个元素的序列当且仅当满足(
且
)或(
且
)可以构建大顶堆。
重点讲述方案二:
这里主要利用了完全二叉树的如下性质(堆也是完全二叉树):
- 对于完全二叉树中的第 i 个数,它的左子节点下标:
- 对于完全二叉树中的第 i 个数,它的右子节点下标:
- 对于有n(n≥2)
个元素的完全二叉树,它的最后一个非叶子结点的下标:n/2−1
以数组[40,60,80,50,90]为例,进行详细讲解:
- 最后一个元素/叶子结点(90)与其所在的子树(60,50,90)进行初赛,初赛最大者站在三人组的根结点。
- 90和60交换之后,所得该小树的根节点90再和其父节点进行比较交换:
- 40和90交换之后,40再和其左右子节点再次进行比较并交换(此过程称为下沉):
- 左子树排列完成,开始调整堆。取出堆顶数字,并和最后一个元素进行交换:
- 根节点40再和其左右结点进行比较,择其大者放至根节点:
- 节点40再找其子节点再进行比较,发现无子节点,无需调整。
- 将新得到的根节点80和最后一个元素(即左右一个叶子节点)进行交换
- 与最后一个元素交换之后,再将新的根节点元素50和其左右子结点进行比较交换,择其大者作为新的根节点:
- 交换之后将交换过后的子结点50继续向下比较,但5已经到达叶子结点(因为80和90不能再动了),因此无需调整。将根节点60与其最后一个子节点40进行交换,调整之后最后一个叶子结点就标记为不可动。
- 将新根节点40和其左节点50进行比较后交换。交换后新子节点40需要继续向下比较,但其左右节点已被标记为不可动,或者说40现在已经是叶子结点,因此40无需再调整。
- 将新根节点50和最后一个叶子结点/元素进行交换:
- 由于根节点的左右子节点都被标记为不可动,或者说根结点只有它自己,此时排序完成:
代码实现如下:
#include <stdio.h>
void swap(int* nums,int index_a, int index_b) {
int tmp = nums[index_b];
nums[index_b] = nums[index_a];
nums[index_a] = tmp;
}
void maxHeapify(int* nums, int index, int curHeapSize) {
int leftIndex = 2 * index + 1;
int rightIndex = 2*index + 2;
int maxIndex = index;
if (leftIndex >= curHeapSize || rightIndex >= curHeapSize) {
return;
}
if (nums[leftIndex] > nums[maxIndex]) {
maxIndex = leftIndex;
}
if (nums[rightIndex] > nums[maxIndex]) {
maxIndex = rightIndex;
}
// 如果最大结点和根结点不一致,则需要交换根节点的位置
if(maxIndex != index) {
swap(nums,index,maxIndex);
// 需要以递归方式继续进行构建大顶堆
maxHeapify(nums,maxIndex,curHeapSize);
}
}
void buildMaxHeap(int* nums,int numsSize) {
// 根据完全二叉树的性质:
// 对于有 n(n≥2)个元素的完全二叉树,它的最后一个非叶子结点的下标为n/2 - 1:
for (int i = numsSize/2 - 1; i >= 0; i--) {
// 将最后一个非叶子结点的树进行大顶堆化,可以理解为将三人组排序
maxHeapify(nums,i,numsSize);
}
}
void heapSort(int* nums, int numsSize) {
// 堆排序算法第一步:构建大顶堆
buildMaxHeap(nums, numsSize);
// 大顶堆构建完成之后,需要将堆顶的根结点交换到最后一个叶子结点
for (int i = numsSize - 1; i>=0; i--) {
swap(nums,0,i); // 将堆顶的元素交换到最后一个叶子结点
maxHeapify(nums,0,i);
}
}
int main() {
printf("堆排序算法(heap Sort Algorithm)\n");
int a[] = {81, 94, 11, 96, 12, 35, 17, 95, 28, 58, 41, 75, 15};
printf("排序前:");
for (int i = 0;i<13;i++) {
printf("%d ",a[i]);
}
heapSort(a, 13);
printf("\n排序后:");
for (int i = 0;i<13;i++) {
printf("%d ",a[i]);
}
return 0;
}
运行结果如下:
堆排序算法(heap Sort Algorithm)
排序前:81 94 11 96 12 35 17 95 28 58 41 75 15
排序后:12 11 15 17 28 35 41 58 75 81 94 95 96