当前位置: 首页 > news >正文

C语言:选择排序算法深度剖析!

引言

本篇博客,我们将深入探讨选择排序这一经典算法。我们将使用C语言这一历史悠久且强大的编程语言,来详细讲解选择排序的实现过程。从算法的定义和思想出发,到具体的代码实现,再到时间复杂度的分析,我们将一步步带您揭开选择排序的神秘面纱。无论您是正在学习数据结构的初学者,还是想巩固算法基础的进阶者,相信都能从本篇文章中有所收获。让我们一起,用C语言编写出优雅而实用的选择排序算法吧!

一、选择排序的核心思想

选择排序的核心思想是:每一次从待排序的数据元素中选出最小(最大)的一个元素,然后存放在序列的起始位置,直到待排数据元素排完

常见的选择排序有直接选择排序和堆排序

二、直接选择排序——以升序为例

2.1、基本框架图解

直接选择排序,从每次的序列范围内选择一个最小的值和序列头部交换位置,然后再更改下一次操作序列的范围。哎,等等,选择排序为什么要交换呢?该算法是从序列中选择一个最值,放到序列头部或者尾部,而交换是为了使数据不丢失的最好办法。

这是以升序为例,挑选最小值放头部的例子:

那么,如何实现上述操作呢?让我们分析分析:

首先,需要确定每次挑选最值的范围,如上述动图,每次挑选最小值放在头部,那么下一次挑选就不能算上头部,这里用for循环颇为合适,进入循环,寻找最小值,然后交换,再进入下一次循环,如下:

//直接选择排序
void SelectionSort(int* arr, int n)
{for (int i = 0; i < n; i++){//找最小值//交换最小值与序列头部Swap(&arr[i], &arr[mini]);}
}

那么,如何寻找最小值呢?

2.2、寻找最小值详解

寻找最小值很简单,这里说明以下,我们寻找的是最小值的下标。

首先,先默认最小值为第一个元素,然后依次遍历后续元素,如果遇见比最小值还要小的元素,那么就更新最小值,直到遍历完成。

代码如下:

int mini = i;
for (int j = i + 1; j < n; j++)
{if (arr[j] < arr[mini]){mini = j;}
}

2.3、完整代码

//直接选择排序
void SelectionSort(int* arr, int n)
{for (int i = 0; i < n; i++){//找最小值int mini = i;for (int j = i + 1; j < n; j++){if (arr[j] < arr[mini]){mini = j;}}//交换最小值与序列头部Swap(&arr[i], &arr[mini]);}
}

2.4、直接选择排序的时间复杂度及优化

通过完整代码不难看出,该算法内外循环都是n,因此时间复杂度为O(N^2),那么,有没有什么办法可以优化呢?有的。

在找最小值的时候,可以最大值和最小值一起寻找,将最小值放在头部,将最大值放在尾部。这个时候,我么可以使用 begin 和 end 来限制寻找的范围。

那就开始写代码:

void SelectionSort(int* arr, int n)
{assert(arr);int begin = 0; int end = n - 1;while (begin < end){int mini = begin;int maxi = begin;for (int i = begin + 1 ; i <= end; i++){if (arr[i] < arr[mini]){mini = i;}if (arr[i] > arr[maxi]){maxi = i;}}if (maxi == begin){maxi = mini;}Swap(&arr[begin], &arr[mini]);Swap(&arr[maxi], &arr[end]);begin++;end--;}
}

注意:在交换的时候前面有个 if 条件,这是为什么呢?

因为会出现这种情况:

当 maxi 等于 begin ,mini 等于 end 的时候,两次交换恰好相互抵消,因此需要判断调整。

那么为什么条件非是 maxi == begin 呢?那如果maxi == begin 而 mini != end 呢?

根本原因就是因为先交换的 arr[begin] 和 arr[mini] ,而后交换的 arr[maxi] 和 arr[end]!那么如果 maix 在 begin 位置的话,就相当于那个位置被交换了两次!不管 mini 在什么地方。

像这种情况,下标为2的地方被交换了两次,显然是不符合设定的。到现在,条件 maxi == begin 搞清楚了,那为什么要执行 maxi = mini 呢?

首先,交换完 mini 和 begin ,maxi 会随着begin的交换跑到 mini 去,因此,将maxi赋值为mini是正确的。

三、堆排序

3.1、堆排序框架介绍

堆是一种完全二叉树,只不过该完全二叉树节点的值的分布是有要求的。

堆的特点:(1)堆是一颗完全二叉树

                  (2)堆中的某个节点总是不大于或不小于其父节点

                  (3)堆顶是最小值或最大值

利用堆顶是最值来对序列进行排序,我们只需要每次取堆顶,再重新建堆即可。

所以具体框架就是这样:

void HeapSort(int* arr, int n)
{//建堆//取堆顶//调整建堆
}

3.2、向下调整法建堆

在介绍堆的时候,我们计算过在堆排序中,向上和向下调整法的时间复杂度,最后得出向下调整法更好,因此,堆排序通常采用向下调整法建堆。本文堆采用顺序表表示。

但是,如果为了一个堆排序而去专门写一个堆的结构,就显得小题大作了,因此,堆排序只是借鉴堆的思想,然后在原数组上改造。

如图,这是原序列,对应的二叉树为:

这是一个乱序的二叉树,要想将其变为堆,就需要进行向下调整,但是,向下调整一次只能调整一个分支,由此可见,向下调整需要进行多次。还有一个问题,从哪里开始调整,根节点,还是叶节点?向下调整,由上而下,自然是把根节点向下调整,但是有一个前提:子树都是排列好的因此,我们需要挨个将子树调整好,然后再进行根节点的调整。

如图:是从编号为2的节点开始调整。(因为叶子节点没必要调整)

 接下来调整编号为1 的节点,但是发现该节点位置很合适,不用调整。接下来调整编号为0 的节点,也就是根节点:

到此,原序列就变成了最小堆。

下面是建堆的代码:

for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{AdjustDown(arr, i, n);
}
//向下调整建堆---最小堆
void AdjustDown(int* arr, int parent ,int n)
{assert(arr);int child = parent * 2 + 1;while (child < n){if (child+1 < n && arr[child + 1] < arr[child]){child++;}if (arr[child] < arr[parent]){Swap(&arr[child], &arr[parent]);}parent = child;child = parent * 2 + 1;}
}

3.3、取堆顶,继续调整堆

现在建完堆之后,就需要取堆顶,然后继续建堆。但是我们需要明确堆的范围,因此引入变量 end 来规范堆的大小

那么,堆顶取完之后放到哪呢?为了避免开拓新的空间,我们就放在堆的末尾。

int end = n - 1;
while (end > 0)
{Swap(&arr[end], &arr[0]);AdjustDown(arr, 0, end);end--;
}

3.4、完整代码和时间复杂度

void HeapSort(int* arr, int n)
{//建堆for (int i = (n - 1-1) / 2; i >= 0; i--){AdjustDown(arr, i, n);}ArrPrint(arr, n);//取堆顶int end = n;while (end > 0){Swap(&arr[end-1], &arr[0]);end--;AdjustDown(arr, 0, end);}
}

外层的时间复杂度为 O(N) ,下来就要看内层的向下调整的时间复杂度了。而向下调整算法最坏的情况也只不过是由根节点到叶子结点,时间复杂度为 O(logN) .因此,向下调整算法的堆排序的时间复杂度为: O(NlogN)

结语

至此,我们已经完整地探讨了选择排序算法的原理、C语言实现以及性能分析。选择排序作为一种基础的排序算法,虽然在效率上并非最佳,但它简单易懂的特性,使其成为我们学习排序算法的良好起点。

通过本篇博客,相信您已经对选择排序有了更深入的理解。您不仅掌握了如何用C语言实现选择排序,还了解了其时间复杂度,并能够将其与其他排序算法进行比较。

在未来的学习中,您可以尝试将选择排序应用于实际场景,并思考如何优化其性能。例如,在数据量较小的情况下,选择排序仍然是一个不错的选择。

感谢您的阅读!希望本篇博客能帮助您更好地理解选择排序。如果您有任何问题或建议,欢迎在评论区留言讨论。让我们一起在数据结构与算法的道路上不断学习和进步!

http://www.dtcms.com/a/315715.html

相关文章:

  • nodejs 编码初体验
  • JAVA无人共享球杆柜系统球杆柜租赁系统源码支持微信小程序
  • 嵌入式硬件中运放的基本控制原理
  • 基于k8s环境下的pulsar常用命令(上)
  • 达梦分布式集群DPC_分布式任务执行拆分流程_yxy
  • 安全测绘之敏感网络资产排查指南
  • 在Linux上部署RabbitMQ、Redis、ElasticSearch
  • Taro Hooks 完整分类详解
  • 深度解析随机森林 API:参数奥秘与调优指南
  • 在AI时代,如何制定有效的职业规划?AI时代职业规划+AI产品经理角色
  • 【学习笔记】NTP时间同步验证
  • Kali Linux 2025.2基于MITRE ATTCK框架
  • DPU(数据处理单元)架构中,SoC(系统级芯片)与FPGA(现场可编程门阵列)之间的数据交互
  • 山东移动e企组网技术分析:底层架构与实现方式
  • 第12届蓝桥杯Scratch_选拔赛_初级组_真题2020年11月21日
  • SpringBoot3.x入门到精通系列:4.2 整合 Kafka 详解
  • Linux第十二讲:线程概念与控制
  • 前端保持和服务器时间同步的方法【使用vue3举例】
  • Qt 音频播放全攻略:常用函数、实战示例与资源获取
  • 升级 Elasticsearch 到新的 AWS Java SDK
  • 基于LDA主题的网络舆情与情感分析——以云南某景区话题为例
  • 8.5 CSS3多列布局
  • 继承知识总结
  • 【AI】提示词与自然语言处理:从NLP视角看提示词的作用机制
  • 【Lua】题目小练8
  • TrackVLA——开放世界下的四足具身视觉跟踪EVT(智能跟随):集目标识别与轨迹规划为一体的VLA,不怕高动态与遮挡
  • JavaWeb02——基础标签及样式(黑马视频笔记)
  • 扩展欧拉定理以及练习题
  • 嵌入式 - 数据结构:循环链表和内核链表
  • 【Unity笔记】Unity TextMeshPro 字体显示为方块的终极解决方案(含中文、特殊字符支持)