【C语言】【十大排序算法】堆排序

什么是堆?

在学习堆排序前先温习下完全二叉树的概念
一棵深度为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个元素的序列当且仅当{k_1,k_2,k_i,...,k_n}满足(k_i <= k_{2i}k_i \leq k_{2i+1})或(k_i \geq k_{2i}k_i \geq k_{2i+1})时,称之为堆。​​​

如何把一个满二叉树看做堆?


如图,对于 0号元素,位于顶,1号元素为左子树根结点,2号元素为右子树根节点,3号元素为1号元素的左子树根节点/叶子,4号元素为1号元素的右子树根节点/叶子。 以此类推...

堆排序思想


用数列构建出一个大顶堆,去除堆顶的数字;
调整剩余的数字,构建出新的大顶堆,再次去除堆顶的数字;
循环往复,完成整个排序。 

堆排序过程:

  1. 用数列构建出一个大顶堆,取出堆顶的数字;
  2. 调整剩余的数字,构建出新的大顶堆,再次取出堆顶的数字;
  3. 循环往复,完成整个排序。

构建大顶堆的两种方式


方案一:从 0 开始,将每个数字依次插入堆中,一边插入,一边调整堆的结构,使其满足大顶堆的要求;
方案二:将整个数列的初始状态视作一棵完全二叉树,自底向上调整树的结构,使其满足大顶堆的要求。

方案一可使用堆定义进行构建:n个元素的序列当且仅当{k_1,k_2,k_i,...,k_n}满足(k_i <= k_{2i}k_i \leq k_{2i+1})或(k_i \geq k_{2i}k_i \geq k_{2i+1})可以构建大顶堆。​

重点讲述方案二:
这里主要利用了完全二叉树的如下性质(堆也是完全二叉树):

  • 对于完全二叉树中的第 i 个数,它的左子节点下标:left = 2i + 1
  • 对于完全二叉树中的第 i 个数,它的右子节点下标:right=left+1
  • 对于有n(n≥2)个元素的完全二叉树,它的最后一个非叶子结点的下标:n/2−1

以数组[40,60,80,50,90]为例,进行详细讲解:

  1. 最后一个元素/叶子结点(90)与其所在的子树(60,50,90)进行初赛,初赛最大者站在三人组的根结点。
  2. 90和60交换之后,所得该小树的根节点90再和其父节点进行比较交换:
  3. 40和90交换之后,40再和其左右子节点再次进行比较并交换(此过程称为下沉):
  4. 左子树排列完成,开始调整堆。取出堆顶数字,并和最后一个元素进行交换:
  5. 根节点40再和其左右结点进行比较,择其大者放至根节点:
  6. 节点40再找其子节点再进行比较,发现无子节点,无需调整。
  7. 将新得到的根节点80和最后一个元素(即左右一个叶子节点)进行交换
  8. 与最后一个元素交换之后,再将新的根节点元素50和其左右子结点进行比较交换,择其大者作为新的根节点:
  9. 交换之后将交换过后的子结点50继续向下比较,但5已经到达叶子结点(因为80和90不能再动了),因此无需调整。将根节点60与其最后一个子节点40进行交换,调整之后最后一个叶子结点就标记为不可动。
  10. 将新根节点40和其左节点50进行比较后交换。交换后新子节点40需要继续向下比较,但其左右节点已被标记为不可动,或者说40现在已经是叶子结点,因此40无需再调整。
  11. 将新根节点50和最后一个叶子结点/元素进行交换:
  12. 由于根节点的左右子节点都被标记为不可动,或者说根结点只有它自己,此时排序完成:

代码实现如下:

#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

  • 20
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

碧波bibo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值