【数据结构的排序算法2】选择排序与堆排序详解

文章详细介绍了选择排序和堆排序的基本思想,包括动态图示、算法实现和时间复杂度分析。选择排序的时间复杂度为O(N^2),而堆排序为O(N*logN)。性能测试显示,堆排序在效率上优于选择排序。
摘要由CSDN通过智能技术生成

🙊 选择排序🙊

💖 基本思想

选择排序的基本思想是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

💖 动态图示

1、在元素集合 arr [ i ]arr [ n - 1 ] 中选择关键码最大(小)的数据元素

2、若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换

3、在剩余的 arr [ i ]arr [ n - 2 ]arr [ i + 1 ]arr [ n - 1 ])集合中,重复上述步骤,直到集合剩余 1 个元素。

在这里插入图片描述

💖 选择排序进阶版

遍历一遍将最小的数和最大找到,将最小的数据放到数组的最左边,将最大的数据放到数组的最右边。

在这里插入图片描述

选出最大最小数据的图示如下:

在这里插入图片描述

代码解析:

1、注意这里 while 的结束条件是 begin < end,因为当数据是奇数个时,beginend 是同一个位置,此时剩下的元素都已经找到了对应的位置,剩下的元素既是最大也是最小,不用再进入循环了

2、当数据是偶数个时,beginend 排完所有数据后会错过,此时不会进入循环

代码如下:

// 选择排序
void SelectSort(int* a, int n)
{
	//让begin为数组的第一个位置,让end为数组的最后一个位置
	int begin = 0, end = n - 1;

	//注意这里while的结束条件是begin < end
	//因为当数据是奇数个时,begin和end是同一个位置,此时剩下的元素都已经找到了对应的位置
	//剩下的元素既是最大也是最小,不用再进入循环了
	//当数据是偶数个时,begin和end排完所有数据后会错过,此时不会进入循环
	while (begin < end)
	{
		//因为选出的最小的数据是和begin位置的数据进行交换,而不是覆盖
		//所以还需要定义两个变量代表数组中最大和最小数的位置
		int mini = begin, maxi = begin;
		for (int i = begin + 1; i <= end; ++i)
		{
			//如果i的位置比mini位置的值小,就更新mini的位置
			if (a[i] < a[mini])
			{
				mini = i;
			}
			//如果i的位置比maxi位置的值大,就更新maxi的位置
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		//经过上述for循环就选出了最大的数据和最小的数据
		//将最小的数据放到最左边的位置
		swap(&a[begin], &a[mini]);
		++begin;
		//将最大的数据放到最右边的位置
		swap(&a[end], &a[maxi]);
		--end;
	}
}

但是上述代码运行起来会出现问题,因为如果出现重叠即 begin == maxi ,说明下次 maxi 被换到了 mini 位置,需要进行修正。

在这里插入图片描述修改的代码如下:

//经过上述for循环就选出了最大的数据和最小的数据
		//将最小的数据放到最左边的位置
		swap(&a[begin], &a[mini]);
		//修正
 		if (maxi == begin)
		{
			maxi = mini;
		}
		++begin;
		//将最大的数据放到最右边的位置
		swap(&a[end], &a[maxi]);
		--end;
	}

💖 选择排序的时间复杂度

因为是遍历一遍选出两个数,再遍历一遍选出两个数,相当于 n + (n-2) + (n-4) + … + 1/0;所以最后算出的时间复杂度为 O(N^2)
跟直接插入排序比较,插入排序的适应性更好,因为对于有序和局部有序,插入排序的效率可以提升。选择排序再任何情况下都是 O(N^2),不管怎样都是选出最大和最小,包括有序或者接近有序的情况。

💖 选择排序性能测试

利用以下程序进行性能测试:

//性能测试函数
void TestOP()
{
	//产生随机数
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	if (a1 == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int* a2 = (int*)malloc(sizeof(int) * N);
	if (a2 == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int* a3 = (int*)malloc(sizeof(int) * N);
	if (a3 == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
	}
	//程序获取调用函数的执行时间,两个函数的执行时间相减就是程序执行的时间
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();
	
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	
	free(a1);
	free(a2);
	free(a3);
}

执行结果如下,可以看到选择排序的效率并不高

在这里插入图片描述

🙊堆排序🙊

💖 基本思想

堆排序即利用堆的思想来进行排序,总共分为两个骤:

1、建堆
升序: 建大堆
降序: 建小堆

2、 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

在这里插入图片描述

💖 堆的向下调整

 因为已经将堆顶的数据和堆后一个结点进行了交换,所以传入参数下标为 0 的结点,让数组首元素不断向下调整到合适的位置。向下调整适用于任何位置的元素,还需要传入堆的大小来控制调整的边界。
思想

  1. 套用公式 child = parent * 2 + 1 计算出左孩子的下标

  2. 向下调整要与左右孩子中小的那个进行调换,所以在计算出左 孩子下标之后要判断左孩子与右孩子中最小的那个,同时还要考虑最后一层中右孩子不存在的情况,当左孩子位于数组最后一个元素时,右孩子的下标一定是超出数组范围的,所以我们使用 child+1 < size 判断右孩子是否存在。

  3. 如果 chlid < parent 则进行交换,并将 child 赋值给parent,计算出新的child,继续迭代。如果不符合则要跳出调整的循环。


调整结束的两种情况
 1、child >= parent,则调整到位,break跳出循环。

 2、如果父结点调整到了最后一层,无需调整时则结束循环。父亲调整到了最后一层时,child一定是超过了size的大小,所以可以设置 child < size 为循环的条件。
在这里插入图片描述

代码如下:

//堆的向下调整
void AdjustDown(HPDataType* a, HPDataType capacity, HPDataType parent)
{
	//默认左孩子比较大
	int child = parent * 2 + 1;
	//当孩子超出数组范围就退出循环
	while (child < capacity)
	{
		//判断如果左孩子小于右孩子就把右孩子作为child变量,因为child永远存的是孩子中比较大的那个
		//当最后不存在右孩子的时候,判断a[child+1]会造成越界访问,此时需要加child+1 < n的判断条件
		if (a[child] < a[child + 1] && child + 1 < capacity)
		{
			child = child + 1;
		}
		//如果孩子大于父亲,就交换他们两个的值,并且更新父亲的位置继续向下调整
		if (a[child] > a[parent])
		{
			HPDataType tmp = a[child];
			a[child] = a[parent];
			a[parent] = tmp;
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}

}

💖 堆排序总代码

以升序为例,降序同理。升序建大根堆,堆顶的数据跟最后位置的数据交换,把最后位置的数据不看做堆里面的数据,向下调整,选出次大的数据,依次类推就能实现升序。

代码如下:

	//堆排序
	//向下调整
	//以大堆为例编写代码
	void AdjustDown(int* a, int size, int parent)
	{
		//默认是左孩子最大
		int child = parent * 2 + 1;
		while (child < size)
		{
			//如果左孩子小于右孩子且右孩子存在的情况下(如最后一排仅一个元素,就没有右孩子),child更新为右孩子
			if (a[child] < a[child + 1] && child + 1 < size)
			{
				child++;
			}
			//找到了孩子中比较大的
			//大根堆,如果孩子大于父亲,交换孩子与父亲的值
			if (a[child] > a[parent])
			{
				swap(a + child, a + parent);
				//更新孩子的位置
				parent = child;
				child = parent * 2 + 1;
			}
			else
				break;
		}
	}

	// 对数组进行堆排序
	void HeapSort(int* a, int n)
	{
		//首先是向下调整的方式建堆,以大堆为例,通过向下调整的方式建立一个大根堆
		//n代表的是元素的个数(本例为10),向下调整需要确定最后一个元素的父亲节点
		//父亲 = (孩子-1)/2,所以最后一个节点位置是n-1,最后一个节点的父亲位置为((n-1)-1)/2
		//从最后一个节点父亲开始进行调整建堆
		for (int i = (n - 1 - 1) / 2; i >= 0; --i)
		{
			AdjustDown(a, n, i);
		}

		//此时已经构建好一个大根堆
		//升序排列需要将大根堆的第一个位置和最后一个位置元素进行交换
		//交换之后将end-1,表示最大的数据已经找到,下次循环就忽略最大数据,进而找次大数据
		//直到end为0说明有效元素个数为0,所有数据都排完了
		int end = n - 1;
		while (end > 0)
		{
			swap(a + end, a + 0);
			AdjustDown(a, end, 0);
			--end;
		}

	}

💖 堆排序的时间复杂度

堆排序的时间复杂度为 O( N * logN),具体计算过程请参照此篇博客: 堆排序的时间复杂度分析

🙊选择排序与堆排序的性能测试🙊

使用以下代码进行性能测试:

//性能测试函数
void TestOP()
{
	//产生随机数
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	if (a1 == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int* a2 = (int*)malloc(sizeof(int) * N);
	if (a2 == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int* a3 = (int*)malloc(sizeof(int) * N);
	if (a3 == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int* a4 = (int*)malloc(sizeof(int) * N);
	if (a4 == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
	}

	//程序获取调用函数的执行时间,两个函数的执行时间相减就是程序执行的时间
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	free(a1);
	free(a2);
	free(a3);
	free(a4);
}

执行结果发现堆排序的性能和希尔排序性能较优,如下图所示:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值