c语言数据结构——八大排序算法实现

八大排序算法

这篇文章是c语言数据结构章节的最后一个部分——排序算法

在我们的日常生活中,排序是无处不在的,比如打开一个购物网站,会发现网站可能会对其上架的商品有多种排序方式:
在这里插入图片描述
随便输入一个商品,平台对其都有对应的排序功能如销量、价格等。再到中高考成绩的排序决定考生排位,又或是学校内成绩的排序,最终都离不开排序。

所以今天这篇文章将重点讲解八大排序算法的思想以及实现。因为重点是思想以及功能能够成功实现,所以排序的对象选择整形数组。最重要的是学习背后的思想。其他类型的数据排序只需要稍微改动一些逻辑即可。

排序算法种类

八大排序算法正好两两对应一组:
在这里插入图片描述
其中有两种在之前已经接触过了,即堆排序和冒泡排序。所以对应的篇幅会少一些。

接下来,我们将对这几个算法进行深度地剖析及实现。请注意,在这篇文章中,所有的排序算法都已升序进行讲解。

选择排序类

如其名,该类排序最重要的二字就是"选择"。即每一次排序的时候都会在给定数组中选取数据,将选定好的数据放在指定的位置。下面让我们来看看是怎么样操作的。

堆排序

堆排序在之前的堆的实现文章中就已经讲过了,在这里只进行简单的回顾。

感兴趣的读者可以翻阅这篇文章:二叉树概念和堆的实现应用

算法思路

堆本质上是完全二叉树,即逻辑结构是二叉树。但是真正存储的时候,为了效率,其底层选择的是使用数组进行存储。

为了排一个升序的数组,那么应当建大堆。这是之前的文章讲过的。

对于建堆,选择使用向下调整算法建堆的时间复杂度为O(N),代码如下所示:

void AdjustDown(HPDataType* a, int Heapsize, int parent) {
   //默认调整小堆

	//向下调整小堆的逻辑:(但是得保证当前已经是一个小堆)
	//从传入的parent位置开始 找到parent左右孩子较小的那个  (注意:可能会没有左右孩子)
	//如果大于较小的那个孩子 就交换 反之退出循环 

	int Parent = parent;
	while ((Parent * 2 + 1) < Heapsize) {
    //如果不是叶子节点就向下调整

		//假设法找到左右孩子较小的那个
		//如果能进入循环 说明有左孩子 但不一定有右孩子
		int Child = Parent * 2 + 1;
		if (Child + 1 < Heapsize && a[Child] < a[Child + 1])  Child = Child + 1; //调为大堆要改第二个符号 第一个不能改

		if (a[Parent] < a[Child]) {
   //想要调整为大堆就改符号 找左右孩子大的 
			swap(&a[Parent], &a[Child]);
			Parent = Child;
		}
		else break;
	}
}

这段代码已经修改为调整大堆的版本。如果想要改为调整小堆的请按注释修改!

既然已经建立大堆了,就可以模仿出堆过程。将最大的数(堆顶数据)出堆,其实就是存储在数组的最后一个位置。但是后续会调整堆的大小减小1,所以就可以近似看成出堆操作。

最后代码的实现如下:

void HeapSort(int* a, int n) {
   

	//向下调整建堆

	/*int begin = (n - 1 - 1) / 2;
	while (begin >= 0) {
		AdjustDown(a, n, begin);
		begin--;
	}*/

	//当然可以使用for循环
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
   
		AdjustDown(a, n, i);
	}

	//模仿出堆过程
	while (n > 0) {
   
		int end = n - 1;
		swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		n--;
	}
}
时间复杂度和空间复杂度

在上述提及的那篇文章中介绍过:
向下调整算法建堆的时间复杂度为O(N),出堆过程时间复杂度为O(N * LogN),所以最后该算法属于是N * LogN级别,所以时间复杂度为O(N * LogN)。

由于该算法没有开辟额外的空间堆数组进行处理,所以空间复杂度为O(1)。
由于已经讲过,便不再赘述。

选择排序

算法思路

这个排序十分简单,即给定一个数组,每次都找到最小值位置,然后与第一个位置的元素交换位置。然后让后面部分的数组执行上述操作。

如下图例子所示:
在这里插入图片描述
对于上面这个思路的代码实现:

void SelectSort(int* a, int n) {
   
	int begin = 0;
	for(int i = 0; i < n - 1; i++){
   
	    int min = i;
	    for(int j = i + 1; j < n; j++){
   
	        if(a[j] < a[min]) 
	           min = j; 
	    }
	    swap(&a[min],&a[i]);//交换操作
	}
}

很容易看见,选择排序算法的本质就是暴力查找数据。但是为了排序的效率,能不能再对这个部分的算法进行一定程度的优化呢?

答案是可以的

算法优化

既然是暴力查找,那就可以每一次暴力查找的时候同时找到当前数组中最大和最小的,然后放在数组的头尾,然后不断地查找下去直到无法查找即可。

这样子相比于上面的暴力查找,虽然方法类似,但是效率高了很多,所需要的时间可以近似看成上面那个算法的一半。

代码实现:

void SelectSort(int* a, int n) {
   
	//begin从0往后走 end从n-1往前走
	//每一次都要找到最大和最小的,最小的放begin位置,最大的放end位置
	//然后begin++   end--
	//直到begin>=end就出这个循环
	int begin = 0, end = n - 1;
	while (begin < end) {
   
		int maxi = begin, mini = begin;//最大数和最小数的下标
		for (int i = begin + 1; i <= end; i++) {
   
			if (a[i] > a[maxi]) {
   
				maxi = i;
			}
			if (a[i] < a[mini]) {
   
				mini = i;
			}
		}
		//交换操作
		
		if (begin == maxi && end == mini) swap(&a[begin], &a[end]);
		else if (begin == maxi) {
   
			swap(&a[end], &a[maxi]);
			swap(&a[begin], &a[mini]);
		}
		else {
   
		    swap(&a[begin], &a[mini]);  
	     	swap(&a[end], &a[maxi]);
		}
		begin++;
		end--;
	}
}

但是为什么后续的交换操作如此麻烦呢?

这是因为当一次性找两个数据的时候,有如下几种可能:
1.最大的数字的坐标与寻找数组段的第一个位置元素重合,最小的数字坐标与数组段最后一个位置元素重合
如果此时先swap(&a[begin], &a[mini]);,再swap(&a[end], &a[maxi]);,就会导致数组实际上是没有交换。所以要特殊处理

2.最大的数字的坐标与寻找数组段的第一个位置元素重合,最小的数字坐标与数组段最后一个位置元素不重合
这一种情况下,如果先swap(&a[begin], &a[mini]);,会导致maxi的位置的值发生改变,指向的是被换后的最小值。那么再执行swap(&a[end], &a[maxi]);的时候,就无法将最大的数放在数组的尾部了,所以也要特殊处理。

3.最大的数字的坐标与寻找数组段的第一个位置元素不重合,最小的数字坐标与数组段最后一个位置元素重合及其余情况
第三种情况的前面半个部分和第2条正好相反,那就与上述操作相反即可。对于其余情况,无论先执行哪一条语句都是正确的,所以不用另外再进行判断。

当然,可以再特殊情况下通过调整maxi和mini的位置。但是在这里为了确保正确,选择逻辑判断。感兴趣的读者可以尝试使用调整坐标方法。

时间复杂度和空间复杂度

无论是算法优化前又或是优化后,这个算法总是会出现两层循环嵌套的逻辑。且每一层循环的次数都与数组长度N有关。所以时间复杂度是很明显的O(N^2),由于比较简单,不进行计算。

没有开辟新的数组对原数组进行处理,所以该算法的时间复杂度是O(1)。

插入排序类

本部分算法的思想就在于”插入“二字。

插入排序

算法思路

一个数组可以看作成已排序区和未排序区。然后从未排序区的第一个数据(记作a)开始,从已排序区的最后一个元素开始向前比较。直到找到第一个小于等于a的数b的时候,就将a插入在b后面的位置。(如果已排序区的数字全部都比a小,那就将a放在第一个位置)

直到已排序区为整个数组的时候,就不用再执行插入算法了。

如图所示:
在这里插入图片描述

代码实现非常简单:

void InsertSort(int* a, int n) {
   
	//思路:把前面的部分当作已排序区(升序) 
	//然后让已排序区的下一个位置的元素跟已排序区内比较
	//直到找到一个小于这个元素的位置,在这个位置后面插入该元素
	for (int i = 0; i < n - 1; i++) {
   
		int end = i; 
		int tmp = a[end + 1]; 
		while (end >= 0) {
    
			if (a[end] > tmp) {
    
				a[end + 1] = a[end]; 
				end--; 
			}
			else break;
		}
		a[end + 1] = tmp; 
	}
}

比tmp小的数会往后挪,直到找到插入位置放入tmp。

注意:这里插入tmp数据尽量不要写在第二层循环内。因为插入排序算法的核心思想就是与前一个位置的数据比较。但是当tmp走到数组第一个位置比较的时候,前面是没有数据的。会退出循环。如果在第二层循环内写插入tmp操作,那么对于刚刚讲到的这个情况需要进行特殊判断。但是写在当前代码显示位置就可以将所有插入情况都满足。故建议写在当前展示的位置。

时间复杂度和空间复杂度

假设最坏的情况下:每一个数字往前插入都要将已排序区的数字全部比较一遍,那么语句执行的次数是一个很明显的等差数列,时间复杂度为O(N^2)。

空间复杂度为O(1),因为没有额外开辟新的空间。

希尔排序

对于刚刚写的插入排序,我们发现,当数组为倒序的时候,每个数据往前插入的时候,都要与已排序区中的所有元素进行排序。那么效率会退化。当数组中元素较多的时候,那么排序的效率并不高。而当算法是升序的时候,每个元素只要比较一遍就可以不用再执行该算法。

也就是说:对于插入排序来讲,数组越接近升序,那排序效率就越高。

针对这个特点,就提出了希尔排序这个算法。这个算法本质上就是对插入排序算法的优化。

算法思路

希尔排序是这样操作的,既然数组越接近升序插入排序效率越高,那就可以先对数组进行若干次预排序,使其较为接近升序。然后再执行上面实现的插入排序操作,这样效率就高得多。

希尔排序会有一些抽象:
它先规定了一个范围叫gap,需要将这个数组分为gap组。

为了方便讲述,我们先假定gap = 3,数组中有十个元素进行讲解:
在这里插入图片描述

如图所示操作,我们会发现所有的数据都被分到了分配的gap组上,且没有重复。

现在这个数组被分为了红、绿、紫三组。希尔排序就是对这红、绿、紫三组指向的元素分别进行预排序。使用的算法就是上面的插入排序算法。只不过此时相邻的数据举例变为了gap = 3,而不是之前的1。

连续对三组预排序的代码编写很困难,不妨先写对红色的预排序,然后再来推广:

//对红色排
for (int end = 0; end < n - gap; end += gap) {
   
	int tmp = a[end + gap];
	//插入排序
	while (end >= 0) {
    
		if (tmp < a[end]) {
     
			a[end +
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值