目录
🙊 选择排序🙊
💖 基本思想
选择排序的基本思想是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
💖 动态图示
1、在元素集合 arr [ i ] 到 arr [ n - 1 ] 中选择关键码最大(小)的数据元素
2、若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
3、在剩余的 arr [ i ] 到 arr [ n - 2 ](arr [ i + 1 ] 到 arr [ n - 1 ])集合中,重复上述步骤,直到集合剩余 1 个元素。
💖 选择排序进阶版
遍历一遍将最小的数和最大找到,将最小的数据放到数组的最左边,将最大的数据放到数组的最右边。
选出最大最小数据的图示如下:
代码解析:
1、注意这里 while 的结束条件是 begin < end,因为当数据是奇数个时,begin 和 end 是同一个位置,此时剩下的元素都已经找到了对应的位置,剩下的元素既是最大也是最小,不用再进入循环了
2、当数据是偶数个时,begin 和 end 排完所有数据后会错过,此时不会进入循环
代码如下:
// 选择排序
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 的结点,让数组首元素不断向下调整到合适的位置。向下调整适用于任何位置的元素,还需要传入堆的大小来控制调整的边界。
思想:
套用公式 child = parent * 2 + 1 计算出左孩子的下标
向下调整要与左右孩子中小的那个进行调换,所以在计算出左 孩子下标之后要判断左孩子与右孩子中最小的那个,同时还要考虑最后一层中右孩子不存在的情况,当左孩子位于数组最后一个元素时,右孩子的下标一定是超出数组范围的,所以我们使用 child+1 < size 判断右孩子是否存在。
如果 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);
}
执行结果发现堆排序的性能和希尔排序性能较优,如下图所示:
文章详细介绍了选择排序和堆排序的基本思想,包括动态图示、算法实现和时间复杂度分析。选择排序的时间复杂度为O(N^2),而堆排序为O(N*logN)。性能测试显示,堆排序在效率上优于选择排序。



修改的代码如下:



153

被折叠的 条评论
为什么被折叠?



