算法-排序2

归并排序,快速排序

本文创作灵感来源于 极客时间 王争老师的《数据结构与算法之美》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

归并排序
  • 归并排序的算法思路:将待排序的数组一分为二,然后分别对两段数组排序,然后合并两段有序数组。
  • 归并排序的核心思想就是分治思想,分而治之。先将大问题一步一步分解成小问题,然后对小问题求解,所有的小问题解决掉之后,大问题也就解决掉了。其中分治的概念就让我们联想到递归,分治是思想,递归是代码实现,分对应了递,治对应了归。
  • 归并排序就是在实现递归的逻辑。实现递归的逻辑,我们首先要推导出递归公式,以及终止条件。公式:sort(list[p,q]) = merge(sort(list[p,r]),sort(list[r+1,q])),终止条件:p >= r。对公式进行下解释,sort 归并排序函数,merge 合并两个有序数组的函数,p 代表数组的起始位置,q 代表数组的末尾位置,r 代表的是数组的中间位置。
  • 归并排序的动图演示:
    在这里插入图片描述

  • 归并排序的代码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    //归并排序
    public static void mergeSort(int[] list,int p,int q, int[] temp){
    //验证左右节点不相等
    if(p < q){
    int r = (p + q)/2;//取中间节点
    mergeSort(list,p,r,temp);//排序左边数组list[p-r]
    mergeSort(list,r+1,q,temp);//排序右边数组list[r+1,q]
    merge(list,p,r,q,temp);//合并数组list[p-r] list[r+1,q]
    }
    }

    //合并
    public static void merge(int[] list,int p, int r, int q, int[] temp){
    int i = p;//获取左边数组第一个元素下标
    int j = r + 1;//获取右边数组第一个元素下标
    int t = 0;//设定临时数组下标0
    while(i <= r && j <= q){
    //左边元素小于右边元素
    if(list[i] <= list[j]){
    temp[t] = list[i];
    t++;
    i++;
    }else{
    //左边元素大于右边元素
    temp[t] = list[j];
    t++;
    j++;
    }
    }
    //将左边剩余元素填充进temp中
    while(i <= r){
    temp[t] = list[i];
    t++;
    i++;
    }
    //将右序列剩余元素填充进temp中
    while(j <= q){
    temp[t] = list[j];
    t++;
    j++;
    }
    t = 0;
    //将temp中的元素全部拷贝到原数组中
    while(p <= q){
    list[p] = temp[t];
    p++;
    t++;
    }
    }
  • 归并排序是稳定排序。当我们合并两边有序数组的时候,对比的两个元素值相等,那就先排左边数组元素再排右边数组元素,这样就能保证算法稳定。

  • 关于时间复杂度的分析,王争老师给出了一个思路:递归求解的过程可以推导出递推公式,那么递归代码的时间复杂度一可以推导出递推公式。即问题 a 可以分解成问题 b 和 c 则解决问题 a 的时间复杂度就等于解决问题 b 和 c 的时间复杂度之和,公式为 T(a) = T(b) + T(c) + K,K 为合并 b 和 c 问题的时间复杂度。那么排序一个长度为 n 的数组,时间复杂度公式就是:T(n) = 2*T(n/2) + n,最后推导出时间复杂度为:O(nlogn)。推导过程我理解的也很生疏,就不总结出来了。最重要的是,根据这个分析问题的过程,我们了解到归并排序的时间复杂度跟数组的有序无序没关系,最好最坏以及平均时间复杂度都是O(nlogn)。
  • 关于空间复杂度的计算并不想时间复杂度的计算那样需要累加。因为在任意时刻 CPU 只有一个函数再执行,也就是每次最多申请 n 个空间。当前函数执行结束后,立即释放掉了申请的内存空间,所以空间复杂度为 O(n), 归并排序不是原地排序。
快速排序
  • 快速排序也叫快排,在待排序数组中任一选中一个元素,然后将数组中值比该元素小的放在左边,大的放在右边,然后再次对已经区分过大小的数组分别重复上述步骤,直到无法分割数组为止。
  • 快速排序的核心思想可以理解为治分,最终的实现也是利用了递归。所以,我们先推导出递归公式:quick_sort(list[p,q]) = quick_sort(list[p,r]),quick_sort(list[r+1,q]),终止条件:p >= r。上述公式中,最关键的就是位置 r 的选取,r 一旦确定之后,再次调用 quick_sort 的时候 r 左右两边的元素分别是小于 位置r的元素 和大于 位置r的元素。

  • 快速排序的动图演示:
    在这里插入图片描述

  • 快速排序的代码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    //快速排序
    public static void quickSort(int[] list, int p, int q){
    if(p < q){
    int r = partition(list,p,q);
    quickSort(list,p,r - 1);
    quickSort(list,r + 1,q);
    }
    }

    //切点选取
    public static int partition(int[] list, int p, int q){
    int i = p;
    int j = p;
    while(j < q){
    if(list[j] < list[q]){
    swap(list,i,j);
    i++;
    }
    j++;
    }
    swap(list,i,q);
    return i;
    }
    //交换数组位置上的元素
    public static void swap(int[] list, int i, int j){
    int temp = list[i];
    list[i] = list[j];
    list[j] = temp;
    }
  • 快速排序的时间复杂度推导跟归并排序的时间复杂度推导是一样的,所以最好和平均时间复杂度都是O(nlogn),但是当我们考虑到极端情况,每次分组都是 1 和 n-1 个元素,则时间复杂度推导公式就是:T(n) = T(n-1)+ T(1) + n,然后复杂的推导,最后得出最坏时间复杂度是 O(n^2)。

  • 快速排序在排序的过程中,空间复杂度为 O(1),所以是原地排序。
  • 由于排序过程中有数据插入操作,所以是一种不稳定的排序算法。
快速排序和归并排序对比
  • 归并排序时间复杂度始终是O(nlogn),但是由于不是原地排序,所以空间复杂度就高。快速排序,最好时间复杂度是O(nlogn),最差是O(n^2),但是却是原地排序,空间复杂度低。所以,如果恰当的选择快速排序的切入点,那么快排的执行效率是高于归并排序的。
  • 理解这两种排序的思路,还是需要好好理解分治思想,理解递归的推导和实现。

总结

初入算法学习,必是步履蹒跚,一路磕磕绊绊跌跌撞撞。看不懂别慌,也别忙着总结,先读五遍文章先,无他,唯手熟尔~
与诸君共勉
这篇文章写了三个小时。。。着实很难理解啊。。。

-------------本文结束感谢您的阅读-------------