首页 > 其他分享 >排序下

排序下

时间:2022-08-26 21:26:44浏览次数:52  
标签:sort 归并 复杂度 数组 排序 分区

目录

归并排序

1.算法原理:
如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
image

用递归代码来实现归并排序

递推公式:
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 之间的数据就也排好序了。
2. 性能分析
1)算法稳定性:
归并排序稳不稳定关键要看merge()函数,也就是两个子数组合并成一个有序数组的那部分代码。在合并的过程中,如果 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(c),那就可以得到这样的递推公式:T(a) = T(b) + T(c) + K,其中K等于将两个子问题b、c的结果合并成问题a的结果所消耗的时间。这里有一个重要的结论:不仅递归求解的问题可以写成递推公式,递归代码的时间复杂度也可以写成递推公式。套用这个公式,那么归并排序的时间复杂度就可以表示为:

T(1) = C;   n=1 时,只需要常量级的执行时间,所以表示为 C。
T(n) = 2*T(n/2) + n; n>1,其中n就是merge()函数合并两个子数组的的时间复杂度O(n)。
T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
     = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
     = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
     ......
     = 2^k * T(n/2^k) + k * n
     ......

当T(n/2^k)=T(1) 时,也就是 n/2k=1,我们得到k=log2 n。将k带入上面的公式就得到T(n)=Cn+nlog2n。如用大O表示法,T(n)就等于O(nlogn)。所以,归并排序的是时间复杂度就是O(nlogn)

3)空间复杂度:归并排序算法不是原地排序算法,空间复杂度是O(n)
因为归并排序的合并函数,在合并两个数组为一个有序数组时,需要借助额外的存储空间。为什么空间复杂度是O(n)而不是O(nlogn)呢?如果我们按照分析递归的时间复杂度的方法,通过递推公式来求解,那整个归并过程需要的空间复杂度就是O(nlogn),但这种分析思路是有问题的!因为,在实际上,递归代码的空间复杂度并不是像时间复杂度那样累加,而是这样的过程,即在每次合并过程中都需要申请额外的内存空间,但是合并完成后,临时开辟的内存空间就被释放掉了,在任意时刻,CPU只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时空间再大也不会超过n个数据的大小,所以空间复杂度是O(n)。

快速排序

  1. 算法原理
    如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot(分区点)。然后遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将povit放到中间。经过这一步之后,数组p到r之间的数据就分成了3部分,前面p到q-1之间都是小于povit的,中间是povit,后面的q+1到r之间是大于povit的。根据分治、递归的处理思想,我们可以用递归排序下标从p到q-1之间的数据和下标从q+1到r之间的数据,直到区间缩小为1,就说明所有的数据都有序了。

递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1… r)

终止条件:
p >= r

具体代码:


// 快速排序,A是数组,n表示数组的大小
quick_sort(A, n) {
  quick_sort_c(A, 0, n-1)
}
// 快速排序递归函数,p,r为下标
quick_sort_c(A, p, r) {
  if p >= r then return
  
  q = partition(A, p, r) // 获取分区点
  quick_sort_c(A, p, q-1)
  quick_sort_c(A, q+1, r)
}

归并排序中有一个merge()合并函数,我们这里有一个partition()分区函数。partition() 分区函数实际上我们前面已经讲过了,就是随机选择一个元素作为 pivot(一般情况下,可以选择 p 到 r 区间的最后一个元素),然后对 A[p...r]分区,函数返回 pivot 的下标。

不考虑空间消耗的话,partition() 分区函数可以写得非常简单。我们申请两个临时数组 X 和 Y,遍历 A[p...r],将小于 pivot 的元素都拷贝到临时数组 X,将大于 pivot 的元素都拷贝到临时数组 Y,最后再将数组 X 和数组 Y 中数据顺序拷贝到 A[p....r]。

image

  1. 性能分析
  • 算法稳定性:
    因为分区过程中涉及交换操作,如果数组中有两个8,其中一个是pivot,经过分区处理后,后面的8就有可能放到了另一个8的前面,先后顺序就颠倒了,所以快速排序是不稳定的排序算法。比如数组[1,2,3,9,8,11,8],取后面的8作为pivot,那么分区后就会将后面的8与9进行交换。
  • 时间复杂度:最好、最坏、平均情况
    快排也是用递归实现的,所以时间复杂度也可以用递推公式表示。
    如果每次分区操作都能正好把数组分成大小接近相等的两个小区间,那快排的时间复杂度递推求解公式跟归并的相同。
    T(1) = C; n=1 时,只需要常量级的执行时间,所以表示为 C。
    T(n) = 2*T(n/2) + n; n>1
    所以,快排的时间复杂度也是O(nlogn)。
    如果数组中的元素原来已经有序了,比如1,3,5,6,8,若每次选择最后一个元素作为pivot,那每次分区得到的两个区间都是不均等的,需要进行大约n次的分区,才能完成整个快排过程,而每次分区我们平均要扫描大约n/2个元素,这种情况下,快排的时间复杂度就是O(n^2)。
    前面两种情况,一个是分区及其均衡,一个是分区极不均衡,它们分别对应了快排的最好情况时间复杂度和最坏情况时间复杂度。那快排的平均时间复杂度是多少呢?T(n)大部分情况下是O(nlogn),只有在极端情况下才是退化到O(n^2),而且我们也有很多方法将这个概率降低。
  • 空间复杂度:快排是一种原地排序算法,空间复杂度是O(1)

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

  1. 归并排序,是先递归调用,再进行合并,合并的时候进行数据的交换。所以它是自下而上的排序方式。何为自下而上?就是先解决子问题,再解决父问题。

  2. 快速排序,是先分区,在递归调用,分区的时候进行数据的交换。所以它是自上而下的排序方式。何为自上而下?就是先解决父问题,再解决子问题。

思考

1.O(n)时间复杂度内求无序数组中第K大元素,比如4,2,5,12,3这样一组数据,第3大元素是4。

我们选择数组区间A[0...n-1]的最后一个元素作为pivot,对数组A[0...n-1]进行原地分区,这样数组就分成了3部分,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]区间查找。
时间复杂度分析:

第一次分区查找,我们需要对大小为n的数组进行分区操作,需要遍历n个元素。第二次分区查找,我们需要对大小为n/2的数组执行分区操作,需要遍历n/2个元素。依次类推,分区遍历元素的个数分别为n、n/2、n/4、n/8、n/16......直到区间缩小为1。如果把每次分区遍历的元素个数累加起来,就是等比数列求和,结果为2n-1。所以,上述解决问题的思路为O(n)。

标签:sort,归并,复杂度,数组,排序,分区
From: https://www.cnblogs.com/xiayuxue/p/16629289.html

相关文章

  • 归并排序
    packagecom.inforcreation;importjava.util.Arrays;/***归并排序**归并排序(Mergesort)是建立在归并操作上的一种有效的排序算法,*归并排序对序列的元素进......
  • leetcode147:对链表进行插入排序
    packagecom.mxnet;publicclassSolution147{publicstaticvoidmain(String[]args){}/***Q:给定单个链表的头head,使用插入排序对链......
  • 拓扑排序学习笔记
    前言在学习这篇文章之前,你需要了解的算法有:基本图论知识链式前向星(图的一种存储方式)了解队列、栈等简单数据结构的实现,用STL也行。什么是拓扑排序AOV网的定义......
  • 探索JS中Object对象的key及key的排序
    首先,JavaScript中Object对象的key均为string类型的值。不过Object对象可以接受任意类型的值作为它的key,原因在于,我们为某个Object对象设定key的过程中会触发JavaScript的......
  • leetcode148:排序链表
    packagecom.mxnet;publicclassSolution148{publicstaticvoidmain(String[]args){}/***给你链表的头结点head,请将其按升序排列并......
  • 堆排序
    packagecom.lianzhu.filemanage.utils;importjava.util.Stack;/***栈排序*@description:栈的特性:先进后出如空数组【】*@step1:有一串数字4,8,7,9,2,6......
  • el-tree只能同级拖拽排序
    <el-tree:data="treeData"node-key="id"draggable:allow-drop="allowDrop"@node-drop="handleDrop"></el-tree>主要是用到了allow-drop这个方法,然后去......
  • 2022-8-25 剑指offer-字典树-每日一题-二分/排序
    剑指OfferII063.替换单词难度中等25收藏分享切换为英文接收动态反馈在英语中,有一个叫做 词根(root) 的概念,它可以跟着其他一些词组成另一个较长的单词——我......
  • js 对象数组根据对象的某一个属性值来进行数据排序
    1、根据id值从小到大排序//模拟数据varlist=[{"id":5,"name":"小明","age":5},{"id":2,"name":"小红","age":12},{"id":3,"name":......
  • Java-Java集合排序
    一、ListMap排序修订记录版本是否发布2020-01-25v1.0是一、ListMap排序Java中list里面存放map,根据map中的某一个或多个字段进行排序importjava.u......