首页 > 编程语言 >基于C语言的堆排序算法

基于C语言的堆排序算法

时间:2024-09-05 10:52:11浏览次数:12  
标签:大顶 int 元素 堆排序 C语言 算法 数组 节点

一、堆排序概述

        堆排序是一种基于二叉堆数据结构的高效排序算法。它具有稳定的时间复杂度为O(nlogn),适用于大规模数据的排序。堆排序具有原地排序的特点,即不需要额外的存储空间,几个指针变量使用O(1)空间,元素交换和堆化操作都是在原数组上进行的。然而,堆排序的稳定性较差,在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化。

        堆是一种特殊的树形数据结构,分为最大堆和最小堆。最大堆中父节点的值大于或等于任何子节点的值;最小堆中父节点的值小于或等于任何子节点的值。堆可以用数组实现,对于数组中的任意索引 :父节点索引为 (i-1)/2;左子节点索引为 2*i+1;右子节点索引为 2*i+2

二、堆的概念与性质

(一)二叉堆的定义

        二叉堆是一种特殊的堆,分为最大堆和最小堆。在最大堆中,父节点的值总是大于或等于任何一个子节点的值;在最小堆中,父节点的值总是小于或等于任何一个子节点的值。二叉堆可以通过数组实现,这样避免了指针的使用,大大加快了访问速度。对于一个最大堆,如果有一个关键码的集合K={K_{0},K_{1},K_{2},...,K_{n-1}},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:K_{i} >= K_{2i+1}K_{i} >= K_{2i+2} ( i=0,1,2...)。对于一个最小堆,则满足:K_{i} <= K_{2i+1}K_{i} <= K_{2i+2} ( i=0,1,2...)

(二)堆的存储形式

        堆可以用一个一维数组来实现存储。对于数组中的任意索引 i,如果 i 表示父节点,那么左子节点的索引为 2*i+1,右子节点的索引为 2*i+2;如果 i 表示子节点,那么父节点的索引为 (i-1)/2。例如,在一个最小堆中,假设数组为 [2, 4, 5, 7, 8],其中索引为 0 的位置不使用,从索引为 1 的位置开始计数。那么索引为 1 的元素 2 是根节点,它的左子节点为索引为 3 的元素 7,右子节点为索引为 4 的元素 8;索引为 2 的元素 4 的左子节点不存在(因为 2*2+1=5,超出了数组长度),右子节点也不存在;索引为 3 的元素 7 的父节点是索引为 1 的元素 2,它没有子节点;索引为 4 的元素 8 的父节点也是索引为 1 的元素 2,它也没有子节点。这种存储形式使得堆的操作可以直接通过数组索引进行,提高了操作效率。

三、堆排序原理与步骤

(一)构建大顶堆

        堆排序的第一步是构建大顶堆。假设我们有一个待排序的数组,首先需要找到最后一个非叶子节点。根据堆的性质,最后一个非叶子节点的位置是数组长度除以 2 减 1。例如,如果数组长度为 10,那么最后一个非叶子节点的位置是 10/2-1=4。

        从这个最后一个非叶子节点开始,我们逐个节点地向上进行调整,以构建大顶堆。对于每个节点,我们比较它与其左右子节点的值。如果当前节点小于左子节点的值,就交换当前节点和左子树的值;交换完后要检查左子树是否满足大顶堆的性质,不满足则重新调整子树结构。同样,如果当前节点小于右子节点的值,就交换当前节点和右子树的值;交换完后要检查右子树是否满足大顶堆的性质,不满足则重新调整子树结构。

        以数组 [3, 7, 16, 10, 21, 23] 为例,最后一个非叶子节点位置是 6/2-1=2,对应的值是 16。首先比较 16 和它的左子节点 10,由于 16 大于 10,不需要交换。接着比较 16 和它的右子节点 21,由于 16 小于 21,交换这两个值,此时数组变为 [3, 7, 21, 10, 16, 23]。交换后需要检查右子树是否满足大顶堆的性质,这里右子树只有一个节点 23,满足大顶堆性质,无需调整。然后继续向上调整前一个非叶子节点 7,重复上述过程,直到根节点。当所有非叶子节点都调整完毕后,大顶堆构建完成。

(二)交换与调整

        完成大顶堆的构建后,我们开始进行排序操作。首先,将堆顶元素与末尾元素交换,此时末尾元素就是当前最大元素。然后,对剩余的元素重新调整为大顶堆。

        例如,对于上面构建好的大顶堆 [23, 7, 21, 10, 16, 3],将堆顶元素 23 与末尾元素 3 交换,得到 [3, 7, 21, 10, 16, 23]。此时,除了最后一个元素 23,剩余的元素 [3, 7, 21, 10, 16] 不再是大顶堆,需要重新调整。

        重新调整的过程与构建大顶堆类似,从第一个非叶子节点开始,逐个节点地向下进行调整。对于每个节点,比较它与其左右子节点的值,如果当前节点小于较大的子节点的值,就交换当前节点和该子节点的值,并继续向下调整,直到满足大顶堆的性质。

        重复上述交换与调整的过程,每次将当前最大元素 “沉” 到数组的末尾,直到整个数组排序完成。例如,在第二次交换与调整后,数组可能变为 [16, 7, 21, 10, 3, 23],然后继续进行下一次操作,直到数组完全有序。

四、C 语言实现示例

(一)代码结构分析

以下是对堆排序函数结构的详细分析:

1、调整堆操作

  • 在多个代码示例中,调整堆的操作通常是通过递归实现的。以构建大顶堆为例,从给定的节点开始,比较该节点与其左右子节点的值。如果当前节点小于较大的子节点,则进行交换,并继续对交换后的子节点进行调整,以确保满足大顶堆的性质。
  • 例如,在代码示例中常见的调整堆函数通常具有以下形式:
void Adjust(int* nums, int start, int end) 
{    
    int father = start;         // father 代表当前要调整的父节点位置
    int child = 2 * father + 1; // child 代表父节点的左子节点位置
    while (child <= end) 
    {        
        if (child + 1 <= end && nums[child] < nums[child + 1]) 
        // 如果右子节点存在且右子节点值大于左子节点值,则 child 指向右子节点
        {
            child++;
        }
        
        if (nums[child] > nums[father]) 
        // 如果子节点值大于父节点值,则交换父节点和子节点的值
        {
            int temp = nums[father];
            nums[father] = nums[child];
            nums[child] = temp;
            // 更新 father 和 child,继续调整以 father 为根的子树
            father = child;
            child = 2 * father + 1;
        } 
        else // 如果子节点值小于等于父节点值,调整结束
        {            
            break;
        }
    }
}
  • 在这个函数中,首先确定当前节点的左右子节点,然后通过比较选择较大的子节点与当前节点进行交换,并不断重复这个过程,直到满足大顶堆的性质。

2、交换元素操作

  • 交换元素的操作通常是通过临时变量实现的。在堆排序过程中,需要将堆顶元素与数组中的其他元素进行交换,以实现将最大元素 “沉” 到数组的末尾。
  • 例如:
   void swap(int* a, int* b) 
    {
       int temp = *a;
       *a = *b;
       *b = temp;
    }
  • 这个函数用于交换两个整数指针所指向的值,在堆排序中用于交换堆顶元素和其他元素。

3、构建大顶堆操作

  • 构建大顶堆通常从最后一个非叶子节点开始,逐个节点地向上进行调整。通过调用调整堆的函数,确保每个节点都满足大顶堆的性质。
  • 例如:
   void buildBigHeap(int* arr, int len) 
   {
       for (int i = floor(len / 2) - 1; i >= 0; i--) 
       {
           // 调用调整堆的函数
           Adjust(arr, i, len - 1);
       }
   }
  • 这个函数通过循环遍历从最后一个非叶子节点开始,调用调整堆的函数,逐步构建大顶堆。

(二)实际运行效果

以下是不同代码示例的运行结果展示:

1、对于一个简单的数组int a[] = {7, 3, 8, 5, 1, 2};,经过堆排序后,得到升序排列的结果为1, 2, 3, 5, 7, 8。

  • 首先构建大顶堆,找到最后一个非叶子节点,按照堆排序的步骤进行调整和交换。经过多次迭代,最终实现了数组的有序排列。
  • 例如,在代码运行过程中,可以通过打印数组的中间结果来观察堆排序的每一步操作。

2、对于一个较大规模的随机数组,如通过以下方式生成随机数组:

   int* create_and_generate_random_array(int size) 
   {
       int* array = (int*)malloc(sizeof(int) * size);
       srand(time(NULL));
       for (int i = 0; i < size; i++) 
       {
           array[i] = rand() % 1000;
       }
       return array;
   }
  • 调用堆排序函数对这个随机数组进行排序,可以看到数组中的元素逐渐按照从小到大的顺序排列。
  • 通过打印排序前后的数组,可以直观地验证堆排序算法的正确性和高效性。

        总的来说,通过不同的代码示例和实际运行效果,可以看出堆排序算法在 C 语言中的实现能够有效地对数组进行排序,具有较高的效率和稳定性。

五、算法特性总结

(一)时间复杂度

        堆排序的时间复杂度为 O(nlogn)。在构建大顶堆的过程中,需要从最后一个非叶子节点开始,逐个节点进行调整,调整的次数与节点的高度有关。对于一个包含 n 个元素的数组,最后一个非叶子节点的位置是 n/2-1,所以构建大顶堆的时间复杂度为 O(n)。在排序过程中,每次交换堆顶元素和末尾元素后,需要对剩余的元素重新调整为大顶堆,调整的次数也是 O(logn)。因此,堆排序的总时间复杂度为 O(nlogn)

(二)空间复杂度

        堆排序是一种原地排序算法,空间复杂度为 O(1)。在排序过程中,只需要几个额外的指针变量来辅助操作,不需要额外的存储空间。元素交换和堆化操作都是在原数组上进行的,不会占用额外的内存空间。

(三)稳定性

        堆排序是不稳定的排序算法。在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化。例如,对于数组 [3, 3', 2],如果在构建大顶堆的过程中,先交换了 3 和 3',然后再交换堆顶元素和堆底元素,此时 3 和 3' 的相对位置就发生了变化。

(四)适用场景

  1. 适用于大规模数据的排序。由于堆排序的时间复杂度为 O(nlogn),并且是原地排序算法,不需要额外的存储空间,所以适用于大规模数据的排序。
  2. 适用于需要快速找到最大或最小元素的场景。可以利用堆的特性,快速找到最大或最小元素,例如在优先队列中。

(五)局限性

  1. 不稳定的排序算法,对于需要保持元素相对顺序的场景不适用。
  2. 对于小规模数据的排序,可能不如一些简单的排序算法高效。例如,对于包含少量元素的数组,插入排序可能比堆排序更快。

标签:大顶,int,元素,堆排序,C语言,算法,数组,节点
From: https://blog.csdn.net/m0_71974552/article/details/141923091

相关文章

  • 今日算法随笔:三数之和
    题目链接:15.三数之和思路排序+双指针采用排序+双指针的方法来解决三数之和问题。首先对数组进行排序,然后通过双指针法,针对每一个固定的元素,从其后的数组部分寻找符合条件的三元组。这样能够避免重复的三元组,且利用排序的性质来优化查找效率。解题过程方法运用排......
  • 数模国赛冲刺 | 预测类创新算法CNN-GRU、CNN-LSTM、CNN-BiGRU、CNN-BiLSTM、CNN-BiGRU
    ​预测算法——CNN-GRU、LSTM、BiGRU、BiLSTM-Attention本文汇总了基于卷积神经网络(CNN)与循环神经网络(RNN)及其变体(如GRU、LSTM、BiGRU、BiLSTM)组合的多种预测算法,深入探讨了这些算法的原理、结构、优缺点以及实际应用场景。此外,本文特别介绍了结合Attention机制的CNN-RNN组合......
  • Nature Communications 单细胞算法 scDist,教你怎么找到重要的细胞亚群与基因!
    生信碱移scDist: 寻找关键细胞亚群与基因的方法单细胞RNA测序(scRNA-seq)使我们能够研究受药物治疗、感染以及癌症等疾病中关键的细胞亚群。为了找到可能影响疾病的细胞亚群乃至基因,我们常常去比较两个或多个组之间显著差异的细胞类型。这里"显著差异"的定义可以是不同方面的,......
  • 编译原理项目——C++实现C语言编译器输出为gcc级汇编(代码/报告材料)
    完整的代码材料见文章末尾以下为核心内容和部分结果项目介绍function.cpp实现了共有的函数lexer.cpp词法分析器get_predict_table.cpp获取预测分析表LR.cpp语法分析generate.cpp语义分析中间代码生成to_asm.cpp目标代码生成部分核心代码LR分析#include"co......
  • 无刷电机控制算法的演变
    一,无刷电机可控制方式的演变1.霍尔有感六步换相不管是方波控制还是正弦波控制,六步换相驱动方法都是最简单的无刷电机驱动方式,驱动原理同有刷电机的驱动方式,只是普通无刷电机为3相六相位,普通有刷电机为单相两相位。只需要在合适的时间使能对应的相位矢量,电机便能正常运转。只要换......
  • 【自动驾驶】控制算法(八)横向控制Ⅰ | 算法与流程
    写在前面:......
  • 有向图最短路径与BFS算法的研究
    有向图最短路径与BFS算法的研究引言有向图G=(V,E)的定义与例子BFS算法及其局限性特定边集E'的构造确认最短路径实现BFS并验证结果(C代码)引言在图论中,寻找最短路径是一个经典问题。广度优先搜索(BFS)是一种在无权重图(即所有边的权重相同)中找到从源节点到所有其他......
  • 有向图的最短路径与BFS算法的局限性分析
    有向图的最短路径与BFS算法的局限性分析引言有向图G=(V,E)的示例图G的邻接表表示问题描述BFS算法回顾BFS在示例图G中的应用及局限性构造E_s并证明BFS的局限性C语言实现及验证分析C语言实现的BFS算法结论引言在图论中,最短路径问题是寻找从一个结点(源结点)到......
  • 解决职业摔跤手分类问题的算法与实现
    解决职业摔跤手分类问题的算法与实现引言问题定义算法设计二分图判定算法步骤伪代码C语言实现引言在职业摔跤界,摔跤手通常被分为“娃娃脸”(“好人”)型和“高跟鞋”(“坏人”)型。在任意一对摔跤手之间,都有可能存在竞争关系。本文的目标是设计一个算法,用于判断......
  • 狐狸算法(FOX)优化BP神经网络原理及Matlab代码
    目录0引言1数学模型2优化方式3Maltab代码3.1伪代码3.2FOX主函数代码3.3FOX-BP4视频讲解0引言狐狸算法(Foxoptimizer,FOX)是由HardiMohammed在2023年提出群智能算法,该算法模拟了自然界中狐狸在捕猎时的觅食。FOX基于测量狐狸和猎物之间的距离来执行有效的跳......