前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >快排查找数组中的第K个最大元素

快排查找数组中的第K个最大元素

作者头像
JavaEdge
发布于 2021-12-07 06:02:50
发布于 2021-12-07 06:02:50
4.2K00
代码可运行
举报
文章被收录于专栏:JavaEdgeJavaEdge
运行总次数:0
代码可运行

冒泡排序、插入排序、选择排序时间复杂度都是O(n2),适合小规模数据排序。

两种时间复杂度为O(nlogn)的排序算法,归并排序和快速排序。这两种排序算法适合大规模数据排序,更常用。

归并排序和快速排序都用到了分治思想。

归并排序

要排序一个数组,先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并,整个数组就有序了。

使用的分治思想,跟递归思想很像。 因为分治算法一般都是用递归实现:

  • 分治是一种解决问题的处理思想
  • 递归是一种编程技巧

二者不冲突。

写递归代码的技巧就是,分析得出递推公式,然后找到终止条件,最后将递推公式翻译成递归代码。

想写出归并排序代码,先写出归并排序递推公式。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
递推公式:
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)/2。当下标从p到q和从q+1到r这两个子数组都排好序之后,再将两个有序子数组合并,这样下标p~r的数据就都排好序了。

有了递推公式,转化成代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 归并排序算法, A是数组,n表示数组大小
merge_sort(A, n) {
  merge_sort_c(A, 0, n-1)
}

// 递归调用函数
merge_sort_c(A, p, r) {
  // 递归终止条件
  if p >= r  then return

  // 取p到r之间的中间位置q
  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(A[p…r], A[p…q], A[q+1…r]):将有序的A[p…q]、A[q+1…r]合并成一个有序数组,并放入A[p…r]。 如下,申请一个临时数组tmp,大小与A[p…r]相同。 两个游标i、j,分别指向A[p…q]、A[q+1…r]的第一个元素。 比较这两个元素A[i],A[j]:

  • A[i]<=A[j],则将A[i]放入临时数组tmp,且i后移一位
  • 否则将A[j]放入到数组tmp,j后移一位

继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组,再把另一数组中的数据依次加到临时数组的末尾,这时,临时数组中存储的就是两个子数组合并后结果。最后再把临时数组tmp中的数据拷贝到原数组A[p…r]中。

merge()函数伪代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
merge(A[p...r], A[p...q], A[q+1...r]) {
  var i := p,j := q+1,k := 0 // 初始化变量i, j, k
  var tmp := new array[0...r-p] // 申请一个大小跟A[p...r]一样的临时数组
  while i<=q AND j<=r do {
    if A[i] <= A[j] {
      tmp[k++] = A[i++] // i++等于i:=i+1
    } else {
      tmp[k++] = A[j++]
    }
  }
  
  // 判断哪个子数组中有剩余的数据
  var start := i,end := q
  if j<=r then start := j, end:=r
  
  // 将剩余的数据拷贝到临时数组tmp
  while start <= end do {
    tmp[k++] = A[start++]
  }
  
  // 将tmp中的数组拷贝回A[p...r]
  for i:=0 to r-p do {
    A[p+i] = tmp[i]
  }
}

merge()合并函数如果借助哨兵,代码就会简洁很多。

性能分析

分析排序算法的三个问题:

稳定性

关键看merge():两个有序子数组合并成一个有序数组时。

合并过程中,若A[p…q]和A[q+1…r]之间有值相同的元素,则可像伪代码中那样,先把A[p…q]中的元素放入tmp数组。这就保证值相同的元素,在合并前后的先后顺序不变。所以,归并排序是个稳定排序算法。

时间复杂度

归并排序涉及递归,分析稍有点复杂。

  • 递归的适用场景 一个问题A可分解为多个子问题B、C,则求解问题A即可分解为求解问题B、C。问题BC解决后,再把BC的结果合并成A的结果。

若定义求解问题A的时间是T(A),可得递推关系式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
T(A) = T(B) + T(C) + P

P = 子问题BC的结果合并成问题A的结果所消耗时间

可见递归求解的问题可写成递推公式,递归代码的时间复杂度也可写成递推公式

假设对n个元素归排需时间T(n),分解成两个子数组排序的时间都是T(n/2)。 merge()合并两个有序子数组的时间复杂度是O(n)。 所以,套用前面的公式,归并排序的时间复杂度的计算公式就是:

n=1:
T(1) = C;
n>1:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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(n/2^k)+knT(n/2^k) = T(1) =》 n/2^k=1 =》 k=log2n 将k值代入上面公式=》 T(n)=Cn+nlog2n 用大O标记法表示: T(n)=O(nlogn) 所以归并排序的时间复杂度是 O(nlogn)

归排执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度非常稳定,不管最好情况、最坏情况,还是平均情况,都是O(nlogn)

空间复杂度

归并排序的时间复杂度任何情况下都是O(nlogn),真是个秀儿。 但归并排序并未像快排应用广泛,why? 它有个致命点:不是原地排序算法。

归并排序的合并函数,在合并两个有序数组为一个有序数组时,需借助额外存储空间。

递归代码的空间复杂度不能像时间复杂度那样累加。 尽管每次合并操作都需申请额外内存空间,但合并完成后,临时开辟的内存空间就被释放了。任意时刻,CPU只会有一个函数在执行,也就只会有一个临时内存空间在使用。临时内存空间最大也不会超过n个数据的大小,所以空间复杂度O(n)。

快速排序算法(Quicksort)

快排也是分治思想。乍看有点像归并排序,但思路完全不同。

思想

排序数组中下标从p到r之间的一组数据,选择p到r间任意一数据为pivot(分区点)。

遍历p~r数据:

  • 小于pivot的放到左边
  • 大于pivot的放到右边
  • pivot放到中间

经过这步后,p~r的数据就被分成三部分:

根据分治、递归,可用递归排序下标p ~ q-1的数据和下标q+1 ~ r间的数据,直到区间缩小为1,说明所有数据都有序了。

递推公式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)

终止条件:
p >= r

将递推公式转化成递归代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 快速排序,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()函数:随机选择一个元素作为pivot(一般可选择p~r区间的最后一个元素),然后对A[p…r]分区,函数返回pivot下标。

若不考虑空间消耗,partition()可写得很简单。申请两个临时数组X、Y,遍历A[p…r]:

  • 将<pivot的元素拷贝到X
  • >pivot的元素都拷贝到Y
  • 最后将X、Y中数据顺序拷贝到A[p…r]

但若按照此思路,partition()需很多额外内存空间,那就不是原地排序算法。若希望快排是原地排序算法,则其空间复杂度得O(1),partition()就不能占用太多额外内存空间,需在A[p…r]原地完成分区操作。 原地分区函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
partition(A, p, r) {
  pivot := A[r]
  i := p
  for j := p to r-1 do {
    if A[j] < pivot {
      swap A[i] with A[j]
      i := i+1
    }
  }
  swap A[i] with A[r]
  return i

类似选择排序。通过游标i把A[p…r-1]分成两部分:

  • A[p…i-1]元素都<pivot,称为“已处理区间”
  • A[i…r-1],“未处理区间” 每次从未处理区间取一个元素A[j],与pivot对比:
  • <pivot,则将其加入已处理区间尾部,即A[i]位置

在数组某个位置插入元素,需搬移数据,很耗时。但通过交换,在O(1)时间复杂度内完成插入操作。 这里也是借助这个思想,只需交换A[i]、A[j],即可在O(1)时间复杂度内将A[j]放到下标为i的位置。 分区的整个过程。

分区过程涉及交换操作,如果数组中有两个相同的元素,比如序列 6,8,7,6,3,5,9,4 在经过第一次分区操作之后,两个6的相对先后顺序就会改变。所以,快排不是稳定排序算法。

快排和归并用的都是分治思想,递推公式和递归代码也非常相似,那它们的区别在哪里呢?

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

性能分析

快排也是用递归来实现的。递归代码的时间复杂度,使用之前总结的公式也适用。 每次分区操作,都能正好把数组分成大小接近相等的两个小区间,那快排的时间复杂度递推求解公式跟归并是相同的。所以,快排的时间复杂度也是O(nlogn)。

T(1) = C; n=1时,只需要常量级的执行时间,所以表示为C。
T(n) = 2*T(n/2) + n; n>1

但公式成立前提是每次分区操作,pivot都很合适,正好能将大区间对等地一分为二。但实际很难满足。 极端的:数组数据原已有序,如1,3,5,6,8。如每次选择最后一个元素作为pivot,那每次分区得到的两个区间都不均等。需要进行约n次分区操作,才能完成。每次分区平均要扫描n/2元素,这时快排时间复杂度从 O(nlogn)

退化成 O(n2)

分区极其均衡,分区极其不均衡分别对应快排的最好情况时间复杂度和最坏情况时间复杂度。

平均情况时间复杂度

假设每次分区操作都将区间分成大小9:1的两个小区间。 套用递归时间复杂度的递推公式:

T(1) = C; n=1时,只需要常量级的执行时间,所以表示为C
T(n) = T(n/10) + T(9*n/10) + n; n>1

该公式的递推求解的过程很复杂,不推荐使用。 递归的时间复杂度求解除了递推公式之外,还有递归树: T(n)在大部分情况下的时间复杂度都能做到 O(nlogn) ,只在极端情况下,才会退化到 O(n2) 。有很多方法将这个概率降到很低。

解答

快排核心思想就是分治和分区,可利用分区思想:O(n)时间复杂度内求无序数组中的第K大元素。 如,4, 2, 5, 12, 3这样一组数据,第3大元素就是4。

选择数组区间A[0…n-1]的最后一个元素A[n-1]作为pivot,对数组A[0…n-1]原地分区,这样数组就分成三部分,A[0…p-1]、A[p]、A[p+1…n-1]:

  • K 在A[0…p-1]区间查找
  • p+1=K,则A[p]就是目标
  • K>p+1, 则第K大元素在A[p+1…n-1] 再继续同样思路递归查找A[p+1…n-1]

时间复杂度分析

第一次分区查找,需对大小为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)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/11/10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
CentOs7 LAMP Drupal安装记录
在/etc/sysconfig/network-scripts/ifcfg-eno***中加入
全栈程序员站长
2022/09/09
5060
进击Drupal-1
首先我们需要有一台CentOS6.5以上的主机,如果你还没有使用过Linux的话,本教程就不太适用了。。
零式的天空
2022/03/02
1.4K0
[Drupal学习一]Drupal安装和基本配置[通俗易懂]
大家好,又见面了,我是你们的朋友全栈君。1. 从官方网站上下载drupal-6.16 http://drupal.org/drupal-6.16 2. 将下载的压缩包文件直接解压,放到apache的站点目录中。此时drupal的路径为WEB-SERVER/drupal 3. 访问站点http://localhost:8888/drupal/将进入drupal的安装页面。此时会提醒我们要拷贝重命名.sites/default/default.settings.php为.sites/default/settings.php。 之后刷新页面,继续后续的安装。 4. 在Mysql数据库中建立站点的数据库。此例中取名为drupal_test_site。再返回到drupal安装页面,输入相应的数据库名称及账户信息。点击保存并继续按钮进行数据库的部署。 5. 配置站点信息。包括站点名称 管理员账户信息等,再继续。 6. 如果没有意外,会显示drupal安装成功。
全栈程序员站长
2022/09/09
2.3K0
Drupal安装及使用问题解决列表
修改Apache的配置文件(如httpd.conf),打开 LoadModule rewrite_module modules/mod_rewrite.so选项。然后在<Directory />模块中修改 AllowOverride属性为 ALL。如下所示
全栈程序员站长
2022/09/09
6910
Drupal安装及使用问题解决列表
Drupal Views教程[通俗易懂]
打个比方来说明一下 Views 的作用: Drupal的核心就像一个毛坯房,墙窗户门都有了,也简单的粉刷过了,搬进来也能住;外观主题(Theme)就像室内装修,可以按照自己的喜好来铺地板或是地毯,选择各种各样喜欢的墙纸等等;模块呢,就好比家具,电器之类的,有了模块可以方便实现各种方便的功能,大部分模块都像冰箱电视一样,启动,摆在那里就行了,但是有些模块可以说是大工程,譬如CCK,可以让你建设新屋子,有些是中等工程,譬如views,它可以打掉你屋子之间的墙,改变屋子的格局,Drupal 的是建立在 node 上的,而views 的核心功能就是帮助你改变 node 的组织与显示模式。
全栈程序员站长
2022/08/22
5.9K0
drupal安装心得
大家好,又见面了,我是你们的朋友全栈君。一转眼,原来发现自己两个月没有写blog,
全栈程序员站长
2022/09/09
3.1K0
drupal安装心得
drupal linux安装,在Debian 10(Buster) Linux服务器中安装drupal 8.8.0的说明
按照本说明,你就可以成功的在Debian 10(Buster) Linux服务器中安装好drupal 8.8.0版本,已亲测能稳定运行。
全栈程序员站长
2022/09/09
1.4K0
drupal linux安装,在Debian 10(Buster) Linux服务器中安装drupal 8.8.0的说明
36·Python项目-博客(前后不分离)
-多年互联网运维工作经验,曾负责过大规模集群架构自动化运维管理工作。 -擅长Web集群架构与自动化运维,曾负责国内某大型金融公司运维工作。 -devops项目经理兼DBA。 -开发过一套自动化运维平台(功能如下): 1)整合了各个公有云API,自主创建云主机。 2)ELK自动化收集日志功能。 3)Saltstack自动化运维统一配置管理工具。 4)Git、Jenkins自动化代码上线及自动化测试平台。 5)堡垒机,连接Linux、Windows平台及日志审计。 6)SQL执行及审批流程。 7)慢查询日志分析web界面。
DriverZeng
2022/11/08
8540
36·Python项目-博客(前后不分离)
每周打靶 | Vulnhub-DC1靶机渗透实战
本公众号提供的工具、教程、学习路线、精品文章均为原创或互联网收集,旨在提高网络安全技术水平为目的,只做技术研究,谨遵守国家相关法律法规,请勿用于违法用途,如果您对文章内容有疑问,可以尝试加入交流群讨论或留言私信,如有侵权请联系小编处理。
网络安全自修室
2023/09/02
3060
每周打靶 | Vulnhub-DC1靶机渗透实战
Install Drupal
访问 http://192.168.56.217/drupal/core/install.php
franket
2021/08/11
1.8K0
Drupal表单实例教程[通俗易懂]
转载于:https://my.oschina.net/Qm3KQvXRq/blog/165546
全栈程序员站长
2022/09/01
3400
必备神器:webpack使用入门
昨天,写了入门javascript的一篇文章:这是我的10分钟 js 入门笔记,老铁给我们的建议,js非常有用,具体请看下面:
double
2019/10/22
5740
必备神器:webpack使用入门
前端成神之路-vue前端工程化
小结:推荐使用ES6模块化,因为AMD,CMD局限使用与浏览器端,而CommonJS在服务器端使用。 ES6模块化是浏览器端和服务器端通用的规范.
海仔
2021/03/20
8880
Vulnhnb刷题-DC-8
下载后,导入VMware打开,设置网络连接为NAT,拍摄一个快照防止环境损坏,即可开始攻击。
C3ting
2023/12/26
1490
Vulnhnb刷题-DC-8
从零搭建一个 webpack 脚手架工具(二)
配置完有关 CSS loader 后,还有一个问题,我们不想将 CSS 都插入到 style 标签中,如果 CSS 样式代码很多,会导致生成的 HTML 文件很大,我们希望使用 <link> 标签引入打包后的 CSS 文件(将 CSS 单独提取出来),这时候就要使用一个插件:mini-css-extract-plugin。
多云转晴
2019/12/16
1.5K0
在找一份相对完整的Webpack项目配置指南么?这里有
Webpack已升级为v4版本,优化之后性能提升好几倍,请移步这个 webpack4项目配置Demo,以及 这篇升级优化点
书童小二
2018/09/03
3.6K0
在找一份相对完整的Webpack项目配置指南么?这里有
4. 许愿墙后台管理系统(前端页面)
许愿墙的后台管理系统主要有4个模块:登录模块、首页模块、许愿管理模块和管理员管理模块。使用前后端分离方式,后端接口使用Express框架,前端使用Vue框架,页面使用Element组件。这节实现前端页面。
爱学习的程序媛
2022/04/07
9740
4. 许愿墙后台管理系统(前端页面)
webpack5学习笔记
assetModuleFilename: 'images/contenthash.png'
代码哈士奇
2022/01/26
2.6K0
如何使用Docker Compose安装Drupal
The author selected United Nations Foundation to receive a donation as part of the Write for DOnations program.
全栈程序员站长
2022/09/09
6.2K0
干货 | DC1靶机渗透实战攻略
这次的靶机渗透实战是一个找寻靶机中的flag的过程,并以获得最终的flag为目标。靶机下载地址:(http://www.five86.com/dc-1.html)
网络安全自修室
2021/11/25
1.5K0
干货 | DC1靶机渗透实战攻略
相关推荐
CentOs7 LAMP Drupal安装记录
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 归并排序
    • 性能分析
      • 稳定性
      • 时间复杂度
      • 空间复杂度
  • 快速排序算法(Quicksort)
    • 思想
    • 性能分析
    • 平均情况时间复杂度
  • 解答
    • 时间复杂度分析
加入讨论
的问答专区 >
1高级数据分析师擅长5个领域
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档