[C描述算法入门]常见排序小结

986 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

前言

        常见的几大排序都已经更新完毕了(堆排序早在二叉树讲堆的时候就讲了),本文就来对这些排序算法进行一下小结。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

常见排序小结

排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,排序结果可能会存在不唯一的情况,若经过排序,这些记录的相对次序保持不变,即在原序列中,则称这种排序算法是稳定的,否则称为不稳定的。

       换句话说就是,排序之后,如果相等元素之间原有的相对顺序不变,则称所用的排序方法是稳定的,反之则称之为不稳定的。

微信截图_20210119163314

       例如上图,我们的数组中有两个相同的元素 4, 我们分别用不同的排序算法对其排序,算法一排序之后,两个相同元素的相对位置没有发生改变,我们则称之为稳定的排序算法,算法二排序之后相对位置发生改变,则为不稳定的排序算法。

那排序算法的稳定性又有什么用呢?

       在我们做题中大多只是将数组进行排序,只需考虑时间复杂度空间复杂度等指标,排序算法是否稳定一般不进行考虑。但是在真正软件开发中排序算法的稳定性是一个特别重要的衡量指标。假设我们想要实现年终奖从少到多的排序,然后相同年终奖区间内的红豆数也按照从少到多进行排序。排序算法的稳定性在这里就显得至关重要。这是为什么呢?见下图

img

       第一次排序之后,所有的职工按照红豆数从少到多有序。

       第二次排序中,我们使用稳定的排序算法,所以经过第二次排序之后,年终奖相同的职工,仍然保持着红豆的有序(相对位置不变),红豆仍是从小到大排序。我们使用稳定的排序算法,只需要两次排序即可。

       因为实际生活生产中要进行排序的话完全有可能不止排序一次,每一次排序的衡量标准也可能多种多样,而稳定排序可以让前一次排序的结果服务于后一次排序中数值相等的那些数,让它们保留前一次排序得到的相对顺序。

       上述情况如果我们利用不稳定的排序算法,要实现这一效果的话会比较复杂。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

比较排序和非比较排序

       我们根据元素是否依靠与其他元素的比较来决定元素间的相对次序,来区分比较排序算法和非比较排序算法。

常见的排序算法

image-20220812120415698

       这里有个用来测试各种排序性能的函数,可以在实现排序函数后调用该函数测试性能。

 // 测试排序的性能对比
 void TestOP()
 {
     srand(time(0));
     const int N = 100000;
     int* a1 = (int*)malloc(sizeof(int)*N);
     int* a2 = (int*)malloc(sizeof(int)*N);
     int* a3 = (int*)malloc(sizeof(int)*N);
     int* a4 = (int*)malloc(sizeof(int)*N);
     int* a5 = (int*)malloc(sizeof(int)*N);
     int* a6 = (int*)malloc(sizeof(int)*N);
     
     for (int i = 0; i < N; ++i)
     {
         a1[i] = rand();
         a2[i] = a1[i];
         a3[i] = a1[i];
         a4[i] = a1[i];
         a5[i] = a1[i];
         a6[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();
     int begin5 = clock();
     QuickSort(a5, 0, N-1);
     int end5 = clock();
     int begin6 = clock();
     MergeSort(a6, N);
     int end6 = 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);
     printf("QuickSort:%d\n", end5 - begin5);
     printf("MergeSort:%d\n", end6 - begin6);
     
     free(a1);
     free(a2);
     free(a3);
     free(a4);
     free(a5);
     free(a6);
 }

       以及用来测试排序的OJ题:912. 排序数组 - 力扣(LeetCode)

排序算法总结

       综合而言快排性能最优、使用最多,堆排适合TopK问题,归并适合外排序。

image-20220907220213733

image-20220907220222520

稳定性分析

       实际上这些排序都可以做到不稳定,但如果能通过一些控制来保持稳定就说它是稳定的。

冒泡排序

       你想啊,冒泡排序每次两两比较,比如说我要排升序,当前一个比后一个大时就要两两交换,那要是相等呢?相等时不会交换,继续把后面的数两两比较,所以说它是稳定的。

img

       其实吧,每趟排序都把最大或最小的数一点一点“顶”到已排序序列中,就像泡泡浮上水面一样,遇到两个值相等的情况不会发生交换,所以的确是稳定的。

直接选择排序

       有些资料认为该排序是稳定的,实际上是不稳定的。

img

       无论是原版还是改进版,都能找到使排序不稳定的情况,也就是该排序不能保证稳定。

image-20220916225858135

直接插入排序

       该排序是稳定的,我们实现的逻辑是未排序序列第一个元素和已排序序列从后向前比较,如果未排序序列第一个元素更小,就让已排序序列的元素向后移动“腾出位置”,相等和更大的情况就是直接放在已排序序列元素的后面,所以并不会破坏相等元素的相对位置。

img

希尔排序

       同一组内由于进行直接插入排序所以相同数据相对位置可以不变,但预排序时相同的数据可能被分到不同组,这样就不能保证稳定性。

比如:

image-20220912224648926

堆排序

       直接上一个极端场景:

image-20220912200954173

       其实建堆的时候都可能已经破坏稳定性了,在建完堆后无论数据如何都会按部就班用交换堆顶堆尾、向下调整方法来排序,就比如遇到上图情况,你说稳定不稳定?那肯定不稳定。

快速排序

       你觉得稳定吗?不稳定,就比如下面这个例子:

image-20220912230004133

归并排序

       能控制一下做到稳定,就是在归并判断的时候,多加上一个等于:if (arr[begin1] <= arr[begin2]) tmp[j++] = arr[begin1++]; ,在第一个序列的元素等于第二个序列元素时归并第一个序列的元素,这样就不会破坏稳定性,所以是稳定的。

排序需要掌握的知识点和程度

  1. 几大排序的原理和实现
  2. 时间复杂度和空间复杂度(不要背,要靠理解)
  3. 稳定性
  4. 排序之间特性的对比

以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif