012排序(下)


title: 12排序(下)
date: 2019/12/5
tags: 数据结构与算法
categories:

  • 算法

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

  • 上一节学习了冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是O(n*n),比较高,适合小规模数据的排序。
  • 这节学习复杂度为O(nlogn)的排序算法,归并排序和快速排序。这两种排序算法适合大规模的数据排序,比上节的排序算法更要常用。

归并排序(Merge Sort)

原理

  • 核心思想:如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组都有序了。
  • 归并排序使用的就是分治思想。分治,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。
  • 分治一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧。

实现

  • 递归代码的编写技巧
    • 分析得出递推公式,然后找到终止条件,最后将递推公式翻译成递归代码。
  • 归并排序的递归公式
递推公式:
merge_sort(p..r)=merge(merge_sort(p..q),merge_sort(q+1..r))
终止条件:
p>=r 不用再继续分解
  • 解释递推公式:q等于(p+r)/2。两个子数组都排好序之后,再将两个有序的子数组组合并在一起,这样p到r之间的数据也排好序了。

  • 递归代码

//归并排序,a表示数组,n表示数组大小
public void merge_sort(int[] a,n){
    merge_sort_c(a,0,n-1)
}
//调用递归函数
public int[] merge_sort_c(a,p,r){
    //递归终止条件
    if(p>=r){
        return a;
    }
    //取p到r之间的中间位置q;
    int q=(p+r)/2;
    //分治递归
    merge_sort_c(a,p,q);
    merge_sort_c(a,q+1,r);
    //将a[p...q]和a[q+1...r]合并为a[p...r](伪代码)
    merge(a[p...r],a[p...q],a[q+1...r]);
}

//merge函数的伪代码
merge(a[p...r],a[p...q],a[q+1...r]){
    //初始化变量
    int i=p;
    int j=q+1;
    int k=0;
    //申请一个大小跟a[p...r]一样的临时数组
    int[] tmp=new int[];
    while(i<q&&j<r>){
        if(a[i]<=a[j]){
            tmp[k++]=a[i++];
        }else{
            tmp[k++]=a[j++];
        }
    }
    //判断哪个子数组中有剩余的数据
    int start=i;
    int end=q;
    if(j<=r>){
        start=j;
        end=r;
    }
    //将剩余的数据拷贝到临时数组tmp
    while(start<=end){
        tmp[k++]=a[start++];
    }
    //将tmp中的数组拷贝回a[p...r]
    for(int i=0;i<=r-p;i++){
        a[p+i]=tmp[i];
    }
}

性能分析

  1. 归并排序稳定吗?
    • 在合并过程中,如果a[p…q]和a[q+1…r]之间有值相同的元素,那么可以像伪代码中那样,先把a[p…q]中的元素放入tmp数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以归并排序是一个稳定的排序算法。
  2. 归并排序的时间复杂度?
    • 归并排序涉及递归,时间复杂度计算:
      • 递归适用的场景是,一个问题a可以分解称为多个子问题b、c,那能求解问题a就可以分解为求解问题b、c。问题b、c解决之后,再把b、c的结果合并成a的结果。
      • 如果我们定义求解问题a的时间是T(a),求解问题b、c的时间分别是T(b)和T©,那么我们可以得到这样的递推关系式:T(a)=T(b)+T(c)+K。其中K等于将两个子问题b、c的结果合并成问题a的结果所消耗的时间。
      • 不仅递归求解的问题可以写成递推公式,递归代码的时间复杂度也可以写成递推公式。
    • 推导时间复杂度的递推公式可以得到归并排序的时间复杂度为O(nlogn)。
    • 归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管最好、最坏、还是平均情况,时间复杂度都是O(nlogn)。
  3. 归并排序空间复杂度
    • 不是原地排序算法,因为归并排序的合并函数,合并两个有序数组为一个有序数组时,需要借助额外的存储空间,每次合并之后,临时开辟的内存空间就被释放掉了,所以空间复杂度是O(n)。

快速排序(Quicksort)

原理

  • 核心思想
    • 如果要排序数组中下标从p到r之间的一组数据,我选择p到r之间的任意一个数据作为pivot(分区点)。
    • 遍历p到r之间的数据,将小于pivot的放到左边,小于pivot的放到右边,pivot放在中间。
    • 根据分治递归的思想,用递归排序下标从p到q-1之间的数据和下标从q+1到r之间的数据(q为分区点),直到区间长度缩小为1,就说明所有数据都有序了。

实现

  • 递推公式表示:quick_sort(p…r)=quick_sort(p…q-1)+quick_sort(q+1…r)
  • 终止条件:q>=r
  • 伪代码
//快速排序,a是数组,n表示数组大小
quick_sort(int[] a,n){
    quick_sort_c(a,0,n-1);
}
//快速排序递归函数,p、r为下标
public int[] quick_sort_c(int[] a,p,r){
    if(p>=r){
        return a;
    }
    int q=partition(a[],p,r)//获取分区点
    quick_sort_c(a[],p,q-1);
    quick_sort_c(a[],q+1,r);
    return a;
}
  • partition()分区函数就是随机选择一个元素做为pivot(一般情况下可以选择p到r区间的最后一个元素),然后随a[p…r]分区,函数返回pivot的下标。
  • partition()原地分区函数伪代码
public int partition(int a[],int p,int r){
    int pivot=a[r];
    int i=p;
    for(int j=p;j<r;j++){
        if(a[j]<pivot){
            int tmp=a[i];
            a[i]=a[j];
            a[j]=tmp;
            i++;
        }
    }
    int tmp1=a[i];
    a[i]=a[r];
    a[r]=tmp1;
    return i;
}

归并排序和快速排序的区别

  1. 归并排序的过程是由下到上的,可以先处理子问题,然后再合并。而快速排序是由下到上的,先分区,然后再处理子问题。
  2. 归并排序虽然是稳定的、时间复杂度为O(nlogn)的排序算法,但它是非原地排序算法。快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。

性能分析

  1. 快速排序是一种原地的、不稳定的排序算法。
  2. 如果每次选择的分区点pivot都很合适,正好能将大区间对等地一分为二,快速排序的时间复杂度是O(nlogn),实际上很难实现。
  3. 极端的情况下,如果数组中的数据已经是有序的了,每次选择最后一个元素作为分区点pivot,那么每次得到的区间都是不均等的,我们需要大概n次分区操作,才能完成快排。此时时间复杂度O(n*n)
  4. 大部分情况下,快排时间复杂度都能做到O(nlogn),极端情况下O(n*n),而且也有很多方法降低极端情况概率。(具体方法后续讲)

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

  1. 选择数组区间a[0…n-1]的最后一个元素a[n-1]作为pivot,对数组原地分区,这样就分成了三部分,a[0…p-1]、a[p]、a[p-1…n-1]
  2. 如果p+1=K,那么a[p]就是要求解的元素
  3. 如果K>p+1,说明第K大元素出现在a[p+1…n-1]区间,我们再按照上面的思路递归地在a[p+1…n-1]这个区间内查找。
  4. 同理,如果K< p+1,那么就在a[0…p-1]区间内查找。

课后思考

  1. 现在你有10个接口访问日志文件,每个日志文件大小约为300MB,每个文件里的日志都是按照时间戳从小到大排序的。你希望将这10个较小的日志文件,合并为1个日志文件,合并之后的日志仍然要按照时间戳从小到大排列。如果处理上述排序人物的机器内存只有1GB,你有什么好的解决思路,能“快速”地将这10个日志文件合并吗?
  • 太难了。我不会。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值