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语言实现选择排序,还了解了其时间复杂度,并能够将其与其他排序算法进行比较。

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值