如何分析一个排序算法及常见问题

如何分析一个排序算法?

  1. 算法的执行效率:
    最好、最坏、平均时间复杂度(对于同一阶时间复杂度的排序算法性能对比可能会把系数、常数和低阶考虑到)基于比较的排序算法执行的适合会涉及到元素比较大小和元素的移动与交换,所以分析效率的时,比较次数、交换或者移动的次数也可以考虑进去
  2. 排序算法的内存消耗
    原地排序,指空间复杂度是 O(1) 的排序算法。
  3. 排序算法的稳定性
    待排序序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。eg:2 5 6 3 3 7 1;这组数据由两个3,如果经过排序算法后,两个3的亲啊后顺序没有改变,那么这种排序算法叫稳定的排序算法,反之不稳定。为什么还要考虑稳定性?一些稳定的算法代码的实现会更简洁
  4. 应用场景
  5. 内排序和外排序
    内部排序(一次性可以将所有数据加载到内存中进行排序) 海量数据怎么处理
    外部排序: 数据不需要一次加载到内存

几种排序算法的性能分析

在这里插入图片描述

常见问题

  1. 冒泡排序和插入排序的时间复杂度都是O(n^2),为什么插入排序比冒泡排序更加受欢迎?
    冒泡排序不论怎么优化,交换次数都是固定的,是原始数据的逆序度。而插入排序是无论怎么优化,元素一定的次数也是原始数据的逆序度。但是从代码层面看,冒泡排序的数据交换要比插入排序的数据移动更加复杂,冒泡需要三个赋值操作,而插入只需要一个

    冒泡排序中数据的交换操作:
    if (a[j] > a[j+1]) { // 交换
        int tmp = a[j];
        a[j] = a[j+1];
        a[j+1] = tmp;
        flag = true;
    }
    插入排序中数据的移动操作:
    if (a[j] > value) {
        a[j+1] = a[j]; // 数据移动
    } else {
        break;
    }
    

    如果把执行一个赋值语句的时间粗略的计为时间单位(unit_time).分别用冒泡和插头对同一个逆序度是K的数组进行排序。用冒泡排序,需要 K 次交换操作,每次需要3个赋值语句,所以交换操作总耗时3*K个单位时间。而插入排序中数据移动操作只需要 K 个单位时间。

    还可以写一个性能对比测试程序:随机生成 10000个数组,每个数组包含200个数据,分别冒泡和插入排序算法排序,比较一下时间。

    另外插入排序的算法思路还有优化空间—》希尔排序

  2. 关于冒泡、插入、和选择排序,在小规模数据上用起来还比较高效,但是对于大规模的数据还是快排比较好,但是其实插入排序还是很有用的,一些编程语言中的排序函数的实现原理会用到

  3. 一些特定算法是依赖特定数据结构的,关于冒泡、插入和选择数据常常存储在数组中操作,如果数据存储在链表中,这三种排序算法还能工作吗?如果可以那相应的时间、空间复杂度?

    这需要考虑是否允许修改链表的节点value值,还是说只能改变节点位置
    如果只考虑改变位置:
    冒泡排序:相比于数组实现,比较次数一致,但是交换链表更复杂
    插入排序:比较次数一致,无需搬移,但是排序完毕后有可能出现需要逆置链表
    选择排序:比较次数一致,但是交换操作麻烦
    若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序复杂度系数会减小,选择排序无明显变化

  4. 快排和归并用的都是分治思想,递推公式和代码非常相似,区别在哪里?
    在这里插入图片描述
    从图中可以看出,归并排序的过程是由下到上的,先处理子问题,然后再合并。而快排则相反,他的处理是由上到下,先分区,再处理子问题。归并虽然是稳定时间复杂度为 O(nlogn) 的排序算法,但是它是非原地的。主要是由于合并函数无法原地执行。快排可以实现原地排序,解决了占用太多内存的问题。

  5. 如何用快排思想在O(n)内查找第K大元素?

    比如:4, 2, 5, 12, 3 这样一组数据,第 3 大元素就是 4.

    选择数组区间 arr[0…n-1] 的最后一个元素 arr[n-1]作为pivot(或者用三数取中法找),对数组arr[0…n-1] 原地分区,这样就分成了三部分,A[0…p-1]、A[p]、A[p+1…n-1].
    如果 p+1=K,那 A[p] 就是要求解的元素;如果 K>p+1, 说明第 K 大元素出现在 A[p+1…n-1] 区间,再递归地在 A[p+1…n-1] 这个区间内查找。同理,如果K<p+1,那我们就在 A[0…p-1] 区间查找

  6. 为什么解决上述思路的时间复杂度是 O(n) ?

    因为,第一次分区查找,需要对大小为 n 的数组执行分区操作,需要遍历 n 个元素。第二次分区查找,只需要对大小为 n/2 的数组执行分区操作,需要遍历 n/2 个元素。依次类推,分区遍历元素的个数分别为、n/2、n/4、n/8、n/16.……直到区间缩小为 1。

    如果我们把每次分区遍历的元素个数加起来,就是:n+n/2+n/4+n/8+…+1。这是一个等比数列求和,最后的和等于 2n-1。所以,上述解决思路的时间复杂度就为 O(n)。

    你可能会说,我有个很笨的办法,每次取数组中的最小值,将其移动到数组的最前面,然后在剩下的数组中继续找最小值,以此类推,执行 K 次,找到的数据不就是第 K 大元素了吗?

    不过,时间复杂度就并不是 O(n) 了,而是 O(K * n)。时间复杂度前面的系数是可以忽略吗?O(K * n) 不就等于 O(n) 吗?这个可不能这么简单地划等号。当 K 是比较小的常量时,比如 1、2,那最好时间复杂度确实是 O(n);但当 K 等于 n/2 或者 n 时,这种最坏情况下的时间复杂度就是 O(n2) 了。

  7. 归并和快排
    归并排序和快速排序都用的分治思想,代码通过递归实现,个过程相似,理解归并重点是理解递推公式和merge()合并函数。同理,理解快排的重点也是理解递推公式,还有 partition() 分区函数。

    归并算法是一种在任何情况下时间复杂度都比较稳定的排序算法,致命缺点是不是原地排序,空间复杂度高,是O(n),所以没有快排应用广。

    快排最坏情况时间复杂度是O(n^2),但是平均时间复杂度都是 O(nlogn).而且快排退化到O(n^2)的概率非常小,因为可以通过合理选择pivot来避免。

猜你喜欢

转载自blog.csdn.net/qq_40488936/article/details/105570841