06:排序

1、冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序

2、排序算法的执行效率==》

最好情况、最坏情况、平均情况时间复杂度

时间复杂度的系数、常数、低阶:对同一阶时间复杂度的排序算法性能对比的时候,就要把系数、常数、低阶也考虑进来

比较次数和交换(或移动)次数

3、排序算法的内存消耗==》

原地排序O(1)

4、排序算法的稳定性==》如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变

应用:

比如说,现在要给电商交易系统中的“订单”排序。订单有两个属性,一个是下单时间,另一个是订单金额。如果我们现在有 10 万条订单数据,我们希望按照金额从小到大对订单数据排序。对于金额相同的订单,我们希望按照下单时间从早到晚有序。

先按照下单时间给订单排序。排序完成之后,我们用稳定排序算法,按照订单金额重新排序。两遍排序之后,我们得到的订单数据就是按照金额从小到大排序,金额相同的订单按照下单时间从早到晚排序的。

5、冒泡排序==》冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

优化==》当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作

原地排序、稳定排序

元素操作==》比较与交换,交换次数总数为逆序度=满有序度n*(n-1)/2-初始有序度  O(n**2)

最好情况O(n)、最坏情况\均摊均为O(n**2)

6、插入排序==》核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

元素操作==》比较与移动  需要将一个数据 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 a 插入。

对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数总是固定的,就等于逆序度。

原地排序、稳定排序、最好情况O(n)、最坏情况\均摊均为O(n**2)

如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下,最好是时间复杂度为 O(n)

7、选择排序==》选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

原地排序、最好情况最坏情况时间复杂度都为O(n**2)  不是稳定排序

8、为什么插入排序比冒泡排序受欢迎?==》

冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。

从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个

9、冒泡排序、插入排序、选择排序这三种排序算法,时间复杂度都是O(n**2),比较高,适合小规模数据的排序

    归并排序和快速排序,适合大规模的数据排序

10、如何在O(n)的时间复杂度内查找一个无序数组中的第K大元素==》快排核心思想是分治和分区,可以利用分区的思想,O(n)时间复杂度内求无序数组中的第k大元素

11、归并排序==》核心思想:如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

 

//递推公式:
merge_sort(p...r) = merge(merge_sort(p...q), merge_sort(q+1...r))

//终止条件:
p >= r 不用再继续分解

 

稳定排序、最好情况最坏情况平均情况时间复杂度都为O(nlogn)  空间复杂度O(n)

12、快速排序==》如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。

partition==》通过游标 i 把 A[p…r-1]分成两部分。A[p…i-1]的元素都是小于 pivot 的,我们暂且叫它“已处理区间”,A[i…r-1]是“未处理区间”。我们每次都从未处理的区间 A[i…r-1]中取一个元素 A[j],与 pivot 对比,如果小于 pivot,则将其加入到已处理区间的尾部,也就是 A[i]的位置。在数组某个位置插入元素,需要搬移数据,非常耗时。处理技巧,就是交换,在 O(1) 的时间复杂度内完成插入操作。借助这个思想,只需要将 A[i]与 A[j]交换,就可以在 O(1) 时间复杂度内将 A[j]放到下标为 i 的位置。

原地排序、不稳定 

最好情况、均摊O(logn)  最坏情况O(n**2)

13、归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法

14、题目:有 10 个接口访问日志文件,每个日志文件大小约 300MB,每个文件里的日志都是按照时间戳从小到大排序的。你希望将这 10 个较小的日志文件,合并为 1 个日志文件,合并之后的日志仍然按照时间戳从小到大排列。如果处理上述排序任务的机器内存只有 1GB,你有什么好的解决思路,能“快速”地将这 10 个日志文件合并吗?

15、如何根据年龄给100万用户数据排序==》桶排序

16、线性排序==》桶排序、计数排序、基数排序  O(n)==》三个算法都是非基于比较的排序算法,都不涉及元素之间的比较操作

17、桶排序==》核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

桶排序对要排序数据的要求是非常苛刻的。首先,要排序的数据需要很容易就能划分成 m 个桶,并且,桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序。其次,数据在各个桶之间的分布是比较均匀的。

桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

应用:有 10GB 的订单数据,我们希望按订单金额(假设金额都是正整数)进行排序,但是我们的内存有限,只有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。这个时候该怎么办呢?

 

先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得到,订单金额最小是 1 元,最大是 10 万元。我们将所有订单根据金额划分到 100 个桶里,第一个桶我们存储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 1001 元到 2000 元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02…99)。理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均匀划分到 100 个文件中,每个小文件中存储大约 100MB 的订单数据,我们就可以将这 100 个小文件依次放到内存中,用快排来排序。等所有文件都排好序之后,我们只需要按照文件编号,从小到大依次读取每个小文件中的订单数据,并将其写入到一个文件中,那这个文件中存储的就是按照金额从小到大排序的订单数据了。

18、计数排序==》计数排序其实是桶排序的一种特殊情况。当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。

 

//计数排序,a是数组,n是数组大小。假设数组中存储的都是非负整数
public void countingSort(int[] a, int n) {
    if (n <= 1) return;
    //查找数组中数据的范围
    int max = a[0];
    for (int i = 1; i < n; ++i){
        if (max < a[i]) {
            max = a[i];
        }
    }

    int[] c = new int[max + 1]; //申请一个计数数组c,下标大小[0, max]
    for(int i = 0; i <= max; ++i){
        c[i] = 0;
    }

    //计算每个元素的个数,放入c中
    for (int i = 0; i < n; ++i){
        c[a[i]]++;
    }

    //依次累加
    for (int i = 1; i <= max; ++i){
        c[i] = c[i-1] + c[i];
    } 

    //临时数组r,存储排序之后的结果
    int[] r = new int[n];
    //计数排序的关键步骤
    for (int i = n - 1; i >= 0; --i){
        int index = c[a[i]]-1;
        r[index] = a[i];
        c[a[i]]--;
    }

    //将结果拷贝给a数组
    for (int i = 0; i < n; ++i){
        a[i] = r[i];
    }
}

 

计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

19、基数排序==》有 10 万个手机号码,希望将这 10 万个手机号码从小到大排序,你有什么比较快速的排序方法呢?

可以借助稳定排序的处理思路,先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,以此类推,最后按照第一位重新排序。经过 11 次排序之后,手机号码就都有序了。根据每一位来排序,我们可以用刚讲过的桶排序或者计数排序,它们的时间复杂度可以做到 O(n)。如果要排序的数据有 k 位,那我们就需要 k 次桶排序或者计数排序,总的时间复杂度是 O(k*n)。

基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。

20、实现一个通用的、高性能的排序函数==》优化快速排序

21、qsort==》qsort() 会优先使用归并排序来排序输入数据,因为归并排序的空间复杂度是 O(n),所以对于小数据量的排序,比如 1KB、2KB 等,归并排序额外需要 1KB、2KB 的内存空间,这个问题不大。要排序的数据量比较大的时候,qsort() 会改为用快速排序算法来排序。qsort() 选择分区点的方法就是“三数取中法”。qsort() 是通过自己实现一个堆上的栈,手动模拟递归来解决递归太深会导致堆栈溢出的问题

qsort() 并不仅仅用到了归并排序和快速排序,它还用到了插入排序。在快速排序的过程中,当要排序的区间中,元素的个数小于等于 4 时,qsort() 就退化为插入排序,不再继续用递归来做快速排序,因为我们前面也讲过,在小规模数据面前,O(n2) 时间复杂度的算法并不一定比 O(nlogn) 的算法执行时间长

 

posted @ 2020-08-15 15:25  LinBupt  阅读(239)  评论(0编辑  收藏  举报