排序的基本概念

排序,简单来说就是让无序的数组变得有序。在有序化的过程中,根据几个特点可以进行分类。

一个重要的概念是稳定性。稳定性描述的是关键字相同的两个数值,在排序后是不是会出现为止变化。举例来说,两个元素在数组里放置,数值都是1,如果记前面的1为1+后面的1为1-,则用相应的排序算法排序之后,这两个相等的值是否还能保持1+在前1-在后,如果可以保持则说这个排序算法是稳定的,否则称排序算法不稳定。对于不稳定的排序算法,只需要举出一组关键字实例就可以说明不稳定性。

在排序过程中,根据数据元素是否完全存放在内存中,分为内部排序和外部排序,分别对应排序元素完全放在内存内和完全放在外存。如果是外部排序,相当于数据内容过大,不能一次性全部放进内存进行排序,此时就需要不断的在内存和外存之间进行交换数据。

每个算法都有各自的优缺点,没有最好的算法,只有某种需求下的最优选择。一般对各个算法,考虑的点主要是空间复杂度和时间复杂度。

插入排序

插入排序是一种简单直观的排序方法,其基本思想是每次将一个待排序的记录按其关键字大小插入到前面己排好序的子序列中,直到全部记录插入完成。由插入排序的思想可以引申出三个重要的排序算法: 直接插入排序、 折半插入排序和希尔排序。

直接插入排序,就是将数据分为三部分:已经有序的子序列,待排序的元素,无序的子序列。整个过程都是在重复将从无序子序列中选最左元素作为待排序元素插入有序子序列。

根据这个思路,需要执行n-1次就可以得到一个有序的表,插入过程不需要额外的辅助空间,所以其空间复杂度为O(1),但是在将待排序元素插入有序序列时,需要找位置后插入,必然会引起有序序列元素的后移。

直接插入代码模板为:

void insertSort()
{for(int i=2;i<=n;i++){if(num[i]<num[i-1]){num[0]=num[i];for(int j=i-1;num[0]<num[j];j++)num[j+1]=num[j];num[j+1]=num[0];}}}


对于直接插入排序。空间上没有使用额外的辅助单元,所以空间复杂度为O(1)。时间上最好情况下为数组一开始就是有序的,此时只需要比较一次元素而不用移动元素对应时间复杂度为O(N),而最坏情况下为与目标顺序刚好相反,此时总的比较次数和总的移动次数都是最大的,此情况下对应的时间复杂度为O(N2),因此平均情况下的时间复杂度为O(N2)。稳定性上由于每次插入元素时总是从后向前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序方法。 直接插入排序算法适用于顺序存储和链式存储的线性表。为链式存储时,可以从前往后查找指定元素的位置。

不难看出直接插入排序可以分为两部分:找出插入位置和移动元素。针对于这两个过程,当排序表为顺序表时,可以在查找位置时用二分法代替,即折半插入排序,从而一定程度上降低了时间复杂度,但是限制条件是不能使用链表存储。

对于折半插入排序,仅仅是减少了比较元素的次数而没有减少移动次数,换为二分后,查找过程不依赖于数组的初始状态而是依赖于表中的元素个数,移动次数未变仍然依赖于初始状态。因此总的时间复杂度仍然是O(N2),对于数据量不很大的排序表,折半插入排序往往能表现很好的性能。折半插入排序是一种稳定的排序方法

希尔排序是对插入排序的一种改进。,直接插入排序算法的时间复杂度为 O(N2),但若待排序列为“正序”时, 其时间复杂度可提高至O(N),由此可见它更适用于基本有序的排序表和数据量不大的排序表。希尔排序正是基于这两点分析对直接插入排序进行改进而得来的,又称缩小增量排序。

希尔排序的基本思想是先将待排序表划分为若干的子表,每相隔一定数量的元素就分配在一个子表之内,对每个子表进行直接插入排序,再调整间隔继续直接插入排序。所以希尔排序的过程实际上是由很多个小的直接插入排序组成的,这很多个直接插入排序就是在让大的排序表逐渐变得“基本有序”

对于希尔排序。空间上没有使用额外的存储单元,所以空间复杂度为O(1),时间上由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当 n 在某个特定范围时,希尔排序的时间复杂度约为 O(N1.3)。在最坏情况下希尔排序的时间复杂度为 O(N2)。稳定性方面,当相同的关键字被划分到不同的子表时,可能会改变其相对次序,所以希尔排序是一种不稳定的排序方法。由于涉及了灵活的取节点成子表,所以希尔排序只适用于线性表为顺序存储的情况。

交换排序

交换排序是指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。 基于交换的排序算法很多,主要是冒泡和快排。

冒泡排序的基本思想是: 从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换它们,直到序列比较完。这样扫描一次成为一趟冒泡排序,一趟排序的结果是让最小(最大)元素到达对应位置。下一趟排序时已经确定位置的元素将不再参与排序。这样重复操作,最多经过n-1趟排序就可以使整个序列有序。

冒泡排序模板如下:

void bubbleSort(){int flag=0;for(int i=0;i<n-1;i++){flag=0;for(int j=n-1;j>i;j--)if(num[j]>num[j-1]){swap(num[j],num[j-1]);flag=1;}if(flag==0)break;}}

对于冒泡排序,空间上没有使用额外的排序单元,所以空间复杂度为O(1),根据代码不难看出当初始序列已经是有序的时候,直接跳出循环,所以比较次数为n-1移动次数为0,从而最好情况下时间复杂度为O(N),初始序列为逆序时为最坏情况,需要进行n-1趟排序,每次都让一个元素到达对应位置上,最坏情况下时间复杂度为O(N2),平均情况下时间复杂度也是O(N2)。稳定性方面,由于相邻元素相等时不会交换,所以冒泡排序是一种稳定的排序方法。

快排则是老生常谈的内容了,快排基于分治法。基本思路在于选择哨兵,将排序表的内容根据大于或者小于哨兵区分为两部分,将哨兵放在正确位置后对两部分分别递归。

代码模板如下:
https://blog.csdn.net/weixin_43849505/article/details/101839933

手动模拟快排是最好的理解方法,一般选择最左边的元素作为哨兵,之后从左右两端同时开始遍历,左边的遍历每次选择大于哨兵的元素,右边的遍历每次选择小于哨兵的元素,交换二者,重复这个过程直到两个遍历相遇,这个相遇的位置就是哨兵最后应该放到的位置。不同教材快排的方式会有所区别,严蔚敏版本模拟一次的过程如下:

对于快速排序,空间上由于整个排序过程是递归进行的,所以需要借助一个工作栈来调用信息,其容量与递归深度一致,最好情况为O(LOG2N),最坏情况需要进行n-1次递归,所以空间复杂度为O(N),平均情况下空间复杂度为O(LOG2N)

时间方面,快排的时间与划分是否对称有关,最坏情况是划分一次后所有元素集中在一侧而另一侧一个元素也没有,这种最坏情况下(基本有序或者基本逆序)时间复杂度为O(N2)

有很多方法可以提高算法的效率:一种方法是尽量选取一个可以将数据中分的枢轴元素,如从序列的头尾及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素;或者随机地从当前表中选取枢轴元素,这样做可使得最坏情况在实际排序中几乎不会发生。 最理想的情况每次划分会让左右两部分相近,对应的时间复杂度为O(NLOG2N)

快速排序平均情况下的运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏情况下的运行时间,所以算法中平均性能最优的是快速排序。

稳定性方面, 若右端区间有两个关键字相同,且均小于基准值的记录, 则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的排序方法

选择排序

选择排序作为最暴力的一种,一般都是大一C语言里面的基础排序方法,选择排序的基本思想是每次选择一个最小的元素,与对应位置进行交换,直到所有第n-1趟做完。

对于一般的简单选择排序,就是一个暴力的过程,整个过程不依赖与初始状态,而且比较的次数是固定的,需要按照流程走完这n-1趟排序,模板如下:

void selectSort()
{for(int i=0;i<n;i++){int min=i;for(int j=i+1;j<n;j++)if(a[j]<a[i])min=j;if(min!=i)swap(num[min],num[i]);}
}

对于简单选择排序,空间上不需要额外的辅助单元,所以空间复杂度为O(1)。时间上元素移动的次数很少,但是元素比较的次数却很多,而且与初始状态无关,是固定的n*(n-1)/2次,因此时间复杂度始终是O(N2)。稳定性方面, 在第 i 趟找到最小元素后,和第i个元素交换,可能会导致第 i 个元素与其含有相同关键字元素的相对位置发生改变,所以简单选择排序是一种不稳定的排序方法

选择排序的进阶是堆排序,堆排序在前面树的部分总结过,这里主要补充一下排序的细节。

堆排序的思路就是利用小顶堆或者大顶堆,每次取出根节点上的元素,再将最后面一个元素移动到根节点处,此时整个堆的大小关系被破坏,再通过调整使其重新变成大顶堆或者小顶堆,重复操作就可以得到排序的结果。整个调整过程从下往上,从右往左,破坏的子树需要再次调整。

当向堆中插入元素时也是同样的思路,先将新节点放在顺序的最后一个位置,利用调整算法将其调整为想要的堆即可。

堆排序一定要能够手写模拟整个过程,尤其是在树的示意图中进行表示。堆排序好处在于可以在不用全部排完的前提下获得前面几个元素的排序结果,即可以只排一部分。从而在找第几大元素时很有用。

对于堆排序,空间上没有使用额外的辅助单元,所以空间复杂度为O(1),时间上建堆需要O(N)的时间,之后有n-1次向下调整的操作,每次调整需要O(H),而H为树的高度,所以最好、最坏、平均情况下,时间复杂度都为O(NLOG2N)。稳定性方面,进行筛选时,有可能把后面相同关键字的元素调整到前而,所以堆排序算法是一种不稳定的排序方法

归并排序与基数排序

归并排序是将很多个小的有序子段合并为有序的大子段,根据每次合并的子段个数,可以成为不同路归并排序,最常见的是二路归并排序,示意图如下 :

合并过程中需要使用一个辅助数组,用于暂存待合并的数据,将数据合并到原始数组中去,代码如下:
https://blog.csdn.net/weixin_43849505/article/details/101839845

一般常见的是2-路归并排序算法,空间上用到了一个辅助数组,所以空间复杂度是O(N),时间上没趟归并都需要O(N)的时间,对于2路归并,躺数为向上取整的log2N,所以时间复杂度为O(NLOG2N),对于路数不同的归并排序,时间复杂度也相应地存在差异。稳定性方面,由于合并操作不会改变相对次序,所以归并排序是一种稳定的排序方法

基数排序是一种很特别的方法,它不依赖于比较和移动,而是依赖于关键字中数位的大小。基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。 通过比较关键字中的各个位,来建立顺序。

一般有两种排序方法,一种是最高位优先,另一种是最低位优先,分别对应从关键字开始的两个方向。

基数排序主要涉及两个过程,分配和收集,首先根据要排序的位数,将所有数据分配到不同的位置,之后再将所有元素串在一起。

举例如下:

对于这个三位数序列的排序,如果考虑先排序低位,则看所有数据的个位数,根据个位数的情况将其放在对应的位置上,得到的结果如下:

从结果不难看出,实际上就是一个遍历统计再连接的过程,第一趟排序完成后,再用同样的处理方法处理十位数和百位数,在处理这两趟的过程中都采用上一趟得到的新序列。最终结果为:

对于基数排序,空间上每趟都需要重复使用r个队列,所以空间复杂度为O®,时间上与每个元素的位数有关,如果需要d趟分配与收集,每一趟分配需要O(N)的时间,每一趟收集需要O®的时间,所以总的时间复杂度为O(d(N+R)),稳定性方面,在每一趟排序过程中位数的排序是稳定的,所以基数排序也是稳定的。

各类内部排序算法的比较与应用

作为这部分最重要的知识点,关键在于掌握各个内部排序算法的优缺点,能根据需求选择合适的内部排序算法。一般考虑三个因素:时间空间复杂度、算法的稳定性、算法的过程特征。

从时间角度来看:简单选择排序、直接插入排序和冒泡排序平均情况下都可以达到O(N2)的时间复杂度,而且实现起来也很简单。其中直接插入排序和冒泡排序最优情况下可以达到O(N)的时间复杂度,而简单选择排序与序列的初始状态无关。将这三种排序作为基础,延伸出希尔排序、堆排序、归并排序和快速排序。 希尔排序作为插入排序的拓展,对较大规模的排序都可以达到很高的效率, 但目前未得出其精确的渐近时间。堆排序利用了一种称为堆的数据结构,可在线性时间内完成建堆,且在 O(nlog2n)内完成排序过程。快速排序基于分治的思想,虽然最坏情况下快速排序时间会达到 O(n2),但快速排序平均性能可以达到 O(nlog2n),在实际应用中常常优于其他排序算法。 归并排序同样基于分治的思想,但由于其分割子序列与初始序列的排列无关, 因此它的最好、最坏和平均时间复杂度均为 O(nlog2n)。

与初始状态无关:归并排序、简单选择排序
采用分治思想:归并排序、快速排序

从空间复杂度来看,简单选择排序、插入排序、冒泡排序、 希尔排序和堆排序都可以在自身所在的数据结构内通过交换进行排序。快排需要一个辅助栈来实现递归,根据每次划分的不同栈的大小也各不相同,最优情况为O(LOG2N)最坏则需要O(N)。归并排序需要辅助数组来帮助合并两个子列,大小为O(N) 。

从稳定性的角度,插入排序、冒泡排序、归并排序和基数排序是稳定的排序方法,而简单选择排序、快速排序、希尔排序和堆排序都是不稳定的排序方法。


一般来说选取排序方法要从下面几个角度去考虑:
①待排序的元素数目
②元素本身的信息量大小
③关键字的结构
④稳定性要求
⑤语言工具的条件、存储结构以及辅助空间的限制

常用的规律如下:
①若待排序元素数目较少,选择直接插入排序或者简单选择排序。而这两个排序方式中简单选择排序移动次数较少,所以当记录本身较大时选择简单选择排序。
②若待排序元素已经基本有序,则最好选择直接插入排序或者冒泡排序。
③若待排序元素数目较多,则应采用时间复杂度为O(NLOG2N)的排序方法:快速排序、堆排序或归并排 序。其中,快速排序的平均时间最短,当待排序元素随机分布时用快排最优。而堆排序需要的辅助空间更少,而且不会出现快排的最坏情况,这两种排序方法都是不稳定的,若要求排序必须稳定,则可以选择时间复杂度也是O(NLOG2N)的归并排序。
④若待排序元素数目很大且关键字位数很少可以分解时可以选择基数排序。
⑤信息量本身很大时,为了较少移动所需要的时间,可以采用链表存储。

外部排序

外部排序主要是对大文件的排序,当文件过大以至于无法存入内存时,只能采用外部排序,将数据一部分一部分调入内存,在外部排序的情况下,时间代价主要考虑访问磁盘的数目即IO次数。

外部排序一般采用归并排序,分为两个阶段:根据内存缓冲区大小划分子文件长度后读入内存进行排序和对有序子文件进行归并。当需要与外存交流时,IO时间就格外重要,此时外部排序的总时间=内部排序所需要的时间+外存信息的读写时间+内部归并所需要的时间。

一般来说对r个初始归并段做K路平衡归并,可以用严格k叉树来表示,增大归并路数或者减少初试归并段的个数都可以减少归并趟数,进而减少访问外存的次数,从而提高外部排序的速度。

在多路归并时,可以采用败者树来减少多路归并的耗时。败者树就是通过记录败者,让每棵树的根节点是需要输出的元素,这样子就可以同时进行多路的归并。每次输出根元素后将对应元素从归并段中删去,重新生成第二棵树从而实现进一步的归并。

最佳归并树实际上就是哈夫曼树的变形,这里的考察方法完全按照哈夫曼树就可以,需要注意的地方在于补充空节点,当现有的节点不能够满足严格k叉树的时候,需要通过补充节点来凑出一个严格k叉树。

设度为 0 的结点有 n0个,度为 k 的结点有 nk个,则对严格k叉树有 n0=(k-1)*nk+1,由此可得 nk=(n0-1)/ (k- 1)。

当(n0-1)%(k-1)=0时说明刚好可以构成,不需要补充节点,而结果不为0时,假设取模后结果为u,则说明有u个节点时多余的,需要补充k-u-1个节点来使其可以构造出严格k叉树。

典型题


这道题可以直接去手工模拟排序过程,当排到第五趟时已经全局有序,所以不需要后序的排序。也可以换个角度思考,对于冒泡排序,每一趟都会确定一个元素的最终位置,其余元素的相对位置并不会发生改变,而对于原序列,只有1、2、4、5、6是相对位置错乱的,所以这五个元素排完之后后面的元素就是有序的了。

根据快排的过程,当每次划分都使得元素能够被均匀的分配到两端的时候排序最优,当表本身已经有序或者逆序的时候是最坏的。看选项,D已经是有序的,所以D是最慢情况,A每次划分都可以让左右子段长度相等,所以A是最优情况。


从题目的目标可以看出,这个划分算法需要的是两个子序列数目最接近而且两个子列元素的和要一个最大一个最小,实际上就是把序列从大到小分成了两个部分,根据中位数进行分配,这样就可以实现目标。具体的思路如下:


首先应确定 k1、k2的排序顺序,若先排k1再排 k2,则排序结果不符合题意,排除A、C。再考虑算法的稳定性,当k2 排好序后,再对k1排序,若对k1排序采用的算法是不稳定的,则对于k1相同而k2不同的元素可能会改变相对次序,从而不一定能满足题干中的条件“在k1值相同的情况下,k2 值小的元素在前,大的元素在后”。直接插入排序算法是稳定的,而简单选择排序算法是不稳定的,故只能选 D。

总的来说排序这部分并不难,理顺好各个排序算法的过程、时间空间复杂度、稳定性才是最关键的点。

数据结构 8-0 排序相关推荐

  1. SDUT OJ 数据结构实验之排序一:一趟快排

    数据结构实验之排序一:一趟快排 Time Limit: 1000 ms Memory Limit: 65536 KiB Submit Statistic Discuss Problem Descrip ...

  2. 数据结构 - 简单选择排序法

    数据结构 - 简单选择排序法 在之前的博文里已经介绍过排序的两个基本方法: 双重循环法和冒泡排序法. 基本思想 其实上面两种方法的基本思想都是一样的: 就是将排序步骤分成两层循环, 在内层的每1个循环 ...

  3. SDUT 3400 数据结构实验之排序三:bucket sort

    数据结构实验之排序三:bucket sort Time Limit: 150MS Memory Limit: 65536KB Submit Statistic Problem Description ...

  4. 数据结构实验之排序七:选课名单

    数据结构实验之排序七:选课名单 Time Limit: 1000MS Memory Limit: 65536KB Submit Statistic Problem Description 随着学校规模 ...

  5. 鸿蒙轻内核M核源码分析:数据结构之任务排序链表

    摘要:鸿蒙轻内核的任务排序链表,用于任务延迟到期/超时唤醒等业务场景,是一个非常重要.非常基础的数据结构. 本文会继续给读者介绍鸿蒙轻内核源码中重要的数据结构:任务排序链表TaskSortLinkAt ...

  6. SDUT 3399 数据结构实验之排序二:交换排序

    数据结构实验之排序二:交换排序 Time Limit: 1000MS Memory Limit: 65536KB Submit Statistic Problem Description 冒泡排序和快 ...

  7. 数据结构实验之排序八:快速排序

    数据结构实验之排序八:快速排序 Time Limit: 1000 ms Memory Limit: 65536 KiB Submit Statistic Problem Description 给定N ...

  8. 数据结构实验之排序四:寻找大富翁__咳咳咳,还魂篇!!

    数据结构实验之排序四:寻找大富翁 Time Limit: 200MS  Memory Limit: 512KB Submit  Statistic Problem Description 2015胡润 ...

  9. 数据结构实验之排序四:寻找大富翁 SDUT

    数据结构实验之排序四:寻找大富翁 SDUT Time Limit: 200 ms Memory Limit: 512 KiB Submit Statistic Problem Description ...

  10. 数据结构实验之排序三:bucket sort SDUT

    数据结构实验之排序三:bucket sort SDUT Time Limit: 250 ms Memory Limit: 65536 KiB Submit Statistic Problem Desc ...

最新文章

  1. idea运行jsp显示源码_基于jsp+mysql+Spring+mybatis的SSM在线个人PC电脑商城平台网站系统...
  2. 生成器和生成器表达式
  3. 警告:‘xxxx’ 将随后被初始化
  4. Clojure:导入lein项目到IntelliJ IDEA
  5. 寒江的网站基本优化观点
  6. 化身阿凡达,国外小哥开源 AI 实时变脸工具 Avatarify
  7. 2020 AI、CV、NLP顶会最全时间列表
  8. MTK平台调试LCD步骤浅析
  9. 设计模式系列——单例模式
  10. 记一次docker安装rabbitMq-(简单至极)
  11. python+word+excel+ppt自动化办公教程_Python自动化办公之Word,全网最全看这一篇就够了...
  12. C++视频教程资源链接合集
  13. LM2586S 应用笔记
  14. python三维图像切片成二维_python之画三维图像
  15. 2019 数学建模 省一·国赛(高教杯-C题 “互联网+”时代的出租车资源配置
  16. Linux系统卡死,只有鼠标能动,解决办法
  17. win10硬盘锁怎么解除_win10系统如何解锁bitlocker的硬盘加密
  18. android——webview解决goback()后,界面会刷新的问题
  19. 超级电视与海信电视争第一,这是一场胜负已定的战争
  20. mysql 复制数据库

热门文章

  1. Oracle忽略hint的几种情形
  2. Knockout.js 初探
  3. wxPython--学习笔记
  4. 《javascript高级程序设计》第八章 The Browser Object Model
  5. windows server 2008中IIS7的功能模塊
  6. 【概率论】3-4:二维分布(Bivariate Distribution)
  7. 主定理(Master Theorem)与时间复杂度
  8. 微信帐号被封零钱怎么办?微信针对封停帐号的零钱提取出了一个流程
  9. iptables表与链的相关性图
  10. 我的IBM本本逃过一劫...