排序算法好坏的评定

Posted jetqiu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法好坏的评定相关的知识,希望对你有一定的参考价值。

一、概念扩展

 ------有序度----
1、有序元素对:a[i] <= a[j], 如果i < j; 逆序元素对:a[i] > a[j], 如果 i < j。
2、一组数据中有/逆序元素对的个数即为有/逆序度
3、2,3,1,6这组数据的有序度为4(因为其有有序元素对为4个,分别是(2,3)、(2,6)、(3,6)和(1,6))逆序度为2(因为其有逆序元素对为2个,分别是(2,3)和(2,1))
4、1,2,3,6这样完全有序的数组叫作满有序度;满有序度的计算公式为 n*(n-1)/2;
5、逆序度 = 满有序度 - 有序度

-----原地排序算法---

空间复杂度是 O(1) 的排序算法,如:冒泡排序,插入排序

----稳定排序算法---

如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变

 

二、冒泡排序
1、冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作
2、冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法
3、当有相邻的两个元素大小相等的时候,我们不做交换,此时冒泡排序是稳定的排序算法。
4、冒泡排序每交换一次,有序度就加 1,直到满有序度;
5、冒泡排序最坏情况下,初始状态的有序度是 0,所以要进行 n*(n-1)/2 次交换,最好情况下,初始状态的有序度是 n*(n-1)/2,就不需要进行交换。我们可以取个中间值 n*(n-1)/4,换句话说,平均情况下,需要 n*(n-1)/4 次交换操作,所以平均时间复杂度就是 O(n^2)

 

三、插入排序
1、插入排序是将数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将自己插入,并通过当前插入位置后的已排序区间数据一个一个往后移,原来位置后的未排序区间数据位置不变来保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束
2、插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个原地排序算法
3、对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,此时插入排序是稳定的排序算法
4、在数组中插入一个数据的平均时间复杂度是O(n)。所以,对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以插入排序的平均时间复杂度为 O(n^2)
5、冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个(这也是插入排序要比冒泡排序更受欢迎的原因)

 

四、选择排序
1、选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。唯一不同是选择排序每次会从未排序区间中找到最小的元素,然后与已排序区间最后一个元素的后面的元素交互位置,这样自己就变为已排序区间的最后一个元素
2、选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,所以选择排序空间复杂度是O(1)即原地排序且不是稳定的排序算法
3、选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O(n^2)

 

五、归并排序
1、归并排序是利用分治思想将数据从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了
2、归并排序的递推公式为:merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r)),当p >= r 不用再继续分解;merge_sort(p…r) 表示,给下标从 p 到 r 之间的数组排序。我们将这个排序问题转化为了两个子问题,merge_sort(p…q) 和 merge_sort(q+1…r),其中下标 q 等于 p 和 r 的中间位置,也就是 (p+r)/2。当下标从 p 到 q 和从 q+1 到 r 这两个子数组都排好序之后,我们再将两个有序的子数组合并在一起,这样下标从 p 到 r 之间的数据就也排好序了
3、实现方式:申请一个临时数组 tmp,大小与 A[p…r] 相同。我们用两个游标 i 和 j,分别指向 A[p…q] 和 A[q+1…r] 的第一个元素。比较这两个元素 A[i] 和 A[j],如果 A[i]<=A[j],我们就把 A[i] 放入到临时数组 tmp,并且 i 后移一位,否则将 A[j] 放入到数组 tmp,j 后移一位。继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并之后的结果了。最后再把临时数组 tmp 中的数据拷贝到原数组 A[p…r] 中
4、归并排序的合并函数,在合并两个有序数组为一个有序数组时,需要借助额外的存储空间,所以不是原地排序算法(这也是归并排序并没有像快排那样应用广泛)
5、在合并的过程中,如果 A[p…q] 和 A[q+1…r] 之间有值相同的元素,那我们可以像伪代码中那样,先把 A[p…q] 中的元素放入 tmp 数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法
6、归并排序的时间复杂度是 O(nlogn)
7、归并排序尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)

8、归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的

9、归并排序和快速排序都是时间复杂度为 O(nlogn) 的排序算法,适合大规模的数据排序

 

六、快速排序
1、快速排序也是利用分治思想从要排序数组中下标p 到 r 之间的任意一个数据作为 pivot(分区点,一般情况下可以选择 p 到 r 区间的最后一个元素),将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了
2、快速排序递推公式为:quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1… r),当p >= r终止
3、如果我们不考虑空间消耗的话,partition() 分区函数可以写得非常简单。我们申请两个临时数组 X 和 Y,遍历 A[p…r],将小于 pivot 的元素都拷贝到临时数组 X,将大于 pivot 的元素都拷贝到临时数组 Y,最后再将数组 X 和数组 Y 中数据顺序拷贝到 A[p…r]
4、上面快速排序的实现方式不是原地排序,如果我们希望快排是原地排序算法,可以通过游标 i 把 A[p…r-1] 分成两部分。A[p…i-1] 的元素都是小于 pivot 的,我们暂且叫它“已处理区间”,A[i…r-1] 是“未处理区间”。我们每次都从未处理的区间 A[i…r-1] 中取一个元素 A[j],与 pivot 对比,如果小于 pivot,则将其加入到已处理区间的尾部,也就是 A[i] 的位置(类似选择排序)
5、快排是一种原地、不稳定的排序算法,时间复杂度也是 O(nlogn)

 

七、桶排序

1、桶排序核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了
2、把 n 个数据均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)
3、桶排序必须满足的条件:
3.1、要排序的数据需要很容易就能划分成 m 个桶,并且,桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序
3.2、如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了
4、桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中

 

八、计数排序
1、计数排序是指需要排序数据范围不大且数据划分到桶以后,同一个桶内的数据值都相同的桶排序;如分数是0-900分,有50W考生成绩的排序,根据考生的成绩将50万考生划分到这901个桶里。桶内的数据都是分数相同的考生,依次扫描每个桶,就实现了 50 万考生的排序
2、计数排序必须满足的条件:
2.1、计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了
2.2、计数排序只能给非负整数排序(如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数)

 

九、基数排序
1、基数排序算法是根据每一位来排序,且每一位中又可以用桶排序或者计数排序;如10 万个手机号码,希望将这 10 万个手机号码从小到大排序,先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,以此类推,最后按照第一位重新排序。经过 11 次排序之后,手机号码就都有序了
2、要排序的数据有k位,那我们就需要k次桶排序或者计数排序,总的时间复杂度是 O(k*n)。当 k 不大的时候,比如手机号码排序的例子,k 最大就是 11,所以基数排序的时间复杂度就近似于 O(n)
3、有时候要排序的数据并不都是等长的,如单词的排序,可以把所有的单词补齐到相同长度,位数不够的可以在后面补“0”
4、基数排序必须满足的条件:
4.1、要排序数据可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了
4.2、每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到O(n)了

总结:时间复杂度冒泡、插入、选择都是O(n^2);快排、归并都是O(nlogn);桶、计数、基数都是O(n)

 

以上是关于排序算法好坏的评定的主要内容,如果未能解决你的问题,请参考以下文章

常见的排序算法

数据结构与算法:排序

PHP 四种基本排序算法的代码实现

实用且有效的排序算法

实用且有效的排序算法

最快排序——快速排序(算法三)