如何寻找无序数组中的第K大元素?

有这样一个算法题:有一个无序数组,要求找出数组中的第K大元素。比如给定的无序数组如下所示:

如果k=6,也就是要寻找第6大的元素,很显然,数组中第一大元素是24,第二大元素是20,第三大元素是17...... 第六大元素是9

方法一:排序法

这是最容易想到的方法,先把无序数组从大到小进行排序,排序后的第k个元素自然就是数组中的第k大元素。但是这种方法的时间复杂度是O(nlogn),性能有些差。

方法二:插入法

维护一个长度为k的数组A的有序数组,用于存储已知的K个较大的元素。然后遍历无序数组,每遍历到一个元素,和数组A中的最小元素进行比较,如果小于等于数组A中的最小元素,继续遍历;如果大于数组A中的最小元素,则插入到数组A中,并把曾经的最小元素"挤出去"。

比如K=3,先把最左侧的7,5,15三个数有序放入到数组A中,代表当前最大的三个数。

此时,遍历到3时,由于3<5,继续遍历。

接下来遍历到17,由于17>5,插入到数组A的合适位置,类似于插入排序,并把原先最小的元素5“挤出去”。

继续遍历原数组,一直遍历到数组的最后一个元素......

最终,数组A中存储的元素是24,20,17,代表着整个数组的最大的3个元素。此时数组A中的最小元素17就是我们要寻找的第K大元素。

这个方法的时间复杂度是O(nk),但是如果K的值比较大的话,其性能可能还不如方法一。

小顶堆法

二叉堆是一种特殊的完全二叉树,它包含大顶堆和小顶堆两种形式。其中小顶堆的特点是每一个父节点都小于等于自己的两个子节点。要解决这个算法题,我们可以利用小顶堆的特性。

维护一个容量为K的小顶堆,堆中的K个节点代表着当前最大的K个元素,而堆顶显然是这K个元素中的最小值

遍历原数组,每遍历一个元素,就和堆顶比较,如果当前元素小于等于堆顶,则继续遍历;如果元素大于堆顶,则把当前元素放在堆顶位置,并调整二叉堆(下沉操作)。

遍历结束后,堆顶就是数组的最大K个元素中的最小值,也就是第K大元素

假设K=5,具体操作步骤如下:

1.把数组的前K个元素构建成堆

2.继续遍历数组,和堆顶比较,如果小于等于堆顶,则继续遍历;如果大于堆顶,则取代堆顶元素并调整堆。

遍历到元素2,由于2<3,所以继续遍历。

遍历到元素20,由于20>3,20取代堆顶位置,并调整堆。



遍历到元素24,由于24>5,24取代堆顶位置,并调整堆。



以此类推,我们一个一个遍历元素,当遍历到最后一个元素8时,小顶堆的情况如下:

3.此时的堆顶,就是堆中的最小元素,也就是数组中的第K大元素。

这个方法的时间复杂度是多少呢?

1.构建堆的时间复杂度是O(K)

2.遍历剩余数组的时间复杂度O(n-K)

3.每次调整堆的时间复杂度是O(logk)

其中2和3是嵌套关系,1和2,3是并列关系,所以总的最坏时间复杂度是O((n-k)logk + k)。当k远小于n的情况下,也可以近似地认为是O(nlogk)

这个方法的空间复杂度是多少呢?

刚才我们在详细步骤中把二叉堆单独拿出来演示,是为了便于理解。但如果允许改变原数组的话,我们可以把数组的前K个元素“原地交换”来构建成二叉堆,这样就免去了开辟额外的存储空间。因此空间复杂度是O(1)

代码如下:

/**
* 寻找第k大元素
* @param array 待调整的数组
* @param k 第几大
* @return
*/
public static int findNumberK(int[] array, int k) {
//1.用前k个元素构建小顶堆
buildHeap(array, k);
//2.继续遍历数组,和堆顶比较
for (int i = k; i < array.length; i++) {
if(array[i] > array[0]) {
array[0] = array[i];
downAdjust(array, 0, k);
}
}
//3.返回堆顶元素
return array[0];
} private static void buildHeap(int[] array, int length) {
//从最后一个非叶子节点开始,依次下沉调整
for (int i = (length - 2) / 2; i >= 0; i--) {
downAdjust(array, i, length);
}
} /**
* 下沉调整
* @param array 待调整的堆
* @param index 要下沉的节点
* @param length 堆的有效大小
*/
private static void downAdjust(int[] array, int index, int length) {
//temp保存父节点的值,用于最后的赋值
int temp = array[index];
int childIndex = 2 * index + 1;
while (childIndex < length) {
//如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子
if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
childIndex++;
}
//如果父节点小于任何一个孩子的值,直接跳出
if (temp <= array[childIndex])
break;
//无需真正交换,单项赋值即可
array[index] = array[childIndex];
index = childIndex;
childIndex = 2 * childIndex + 1;
}
array[index] = temp;
} public static void main(String[] args) {
int[] array = new int[] {7, 5, 15, 3, 17, 2, 20, 24, 1, 9, 12, 8};
System.out.println(findNumberK(array, 5));
}

方法四:分治法

大家都了解快速排序,快速排序利用分治法,每一次把数组分成较大和较小元素两部分。我们在寻找第K大元素的时候,也可以利用这个思路,以某个元素A为基准,把大于A的元素都交换到数组左边,小于A的元素交换到数组右边。

比如我们选择以元素7作为基准,把数组分成了左侧较大,右侧较小的两个区域,交换结果如下:

包括元素7在内的较大元素有8个,但我们的K=5,显然较大元素的数目过多了。于是我们在较大元素的区域继续分治,这次以元素12为基准:

这样一来,包括元素12在内的较大元素有5个,正好和K相等。所以,基准元素12就是我们所求的。

这就是分治法的思想,这种方法的时间复杂度甚至优于小顶堆法,可以达到O(n)。

如何寻找无序数组中的第K大元素?的更多相关文章

  1. 寻找无序数组中的前k大元素

    题目描述 以尽可能小的代价返回某无序系列中的两个最大值,当有重复的时设置某种机制进行选择. 题解 首先要考虑的是重复的数的问题. A.不处理重复数据方法:在处理第k大的元素时不处理重复的数据,也就是将 ...

  2. [算法]找到无序数组中最小的K个数

    题目: 给定一个无序的整型数组arr,找到其中最小的k个数. 方法一: 将数组排序,排序后的数组的前k个数就是最小的k个数. 时间复杂度:O(nlogn) 方法二: 时间复杂度:O(nlogk) 维护 ...

  3. 记录我对'我们有成熟的时间复杂度为O(n)的算法得到数组中任意第k大的数'的误解

    这篇博客记录我对剑指offer第2版"面试题39:数组中出现次数超过一半的数字"题解1的一句话的一个小误解,以及汇总一下涉及partition算法的相关题目. 在剑指offer第2 ...

  4. 小米笔试题:无序数组中最小的k个数

    题目描述 链接:https://www.nowcoder.com/questionTerminal/ec2575fb877d41c9a33d9bab2694ba47?source=relative 来 ...

  5. 寻找数组中的第K大的元素,多种解法以及分析

    遇到了一个很简单而有意思的问题,可以看出不同的算法策略对这个问题求解的优化过程.问题:寻找数组中的第K大的元素. 最简单的想法是直接进行排序,算法复杂度是O(N*logN).这么做很明显比较低效率,因 ...

  6. 快速查找无序数组中的第K大数?

    1.题目分析: 查找无序数组中的第K大数,直观感觉便是先排好序再找到下标为K-1的元素,时间复杂度O(NlgN).在此,我们想探索是否存在时间复杂度 < O(NlgN),而且近似等于O(N)的高 ...

  7. 【算法】数组与矩阵问题——找到无序数组中最小的k个数

    /** * 找到无序数组中最小的k个数 时间复杂度O(Nlogk) * 过程: * 1.一直维护一个有k个数的大根堆,这个堆代表目前选出来的k个最小的数 * 在堆里的k个元素中堆顶的元素是最小的k个数 ...

  8. 《程序员代码面试指南》第八章 数组和矩阵问题 找到无序数组中最小的k 个数

    题目 找到无序数组中最小的k 个数 java代码 package com.lizhouwei.chapter8; /** * @Description: 找到无序数组中最小的k 个数 * @Autho ...

  9. 寻找两个已序数组中的第k大元素

    寻找两个已序数组中的第k大元素 1.问题描述 给定两个数组与,其大小分别为.,假定它们都是已按照增序排序的数组,我们用尽可能快的方法去求两个数组合并后第大的元素,其中,.例如,对于数组,.我们记第大的 ...

随机推荐

  1. vagrant之道(即其工作流程)

    原文http://mitchellh.com/the-tao-of-vagrant The Tao of Vagrant 在安装vagrant或了解它如何工作之前,了解vagrant在实际工作环境中的 ...

  2. Sequelize-nodejs-8-Transactions

    Transactions事务 Sequelize supports two ways of using transactions: Sequelize支持两种使用transactions的方法 One ...

  3. idea 如何优雅的添加.ignore 忽略不必要提交的文件

    最近有点时间然后就跑去搜了下idea热门的插件,一不小心发现了一个.ignore插件.有过在idea使用git都知道.gitignore文件,而这个插件就是能优雅的帮我们做这件事. 首先先讲下安装这个 ...

  4. 关于Nanchtiy

    关于我 宇宙超级无敌蒟蒻一枚 爱玩游戏(LOL贼菜) 不爱刷题 不定时更新博客 福建某不知名市的某不知名高中的高二dog 偶尔看看动漫 欢迎加友链啦~ QQ:1468473741(会有妹子加吗?) 我 ...

  5. js随笔记录

     1.当我们尝试优化一段程序的时候,必须要同时了解语言本身和运行环境就比如说,可能教科书上写移位操作比乘法运算要快,但是这是因为CPU指令的问题,所以对于C语言成立,对于跑在VM上的语言来说则不一定了 ...

  6. [笔记] FMX 移动平台 TWebBrowser 问题

    FMX 移动平台下的 TWebBrowser 有一问题: 某些机子当 WebBrowser.Visible := False; 后,依然留在全屏,虽然看不见,但无法点击操作. 解决:用 WebBrow ...

  7. HO引擎近况20180111

    明明想着10号更新了,怎么就忘了呢? 公司的项目又被砍了,几个同事被开,我又转到另外一下组了,难道真的是我到哪哪黄? 快过年了,今年公司年会就在附近,太好了不用想怎么编理由请假事了,还有可能中个奖品, ...

  8. python基础学习1-生成器,递归函数

    #!/usr/bin/env python # -*- coding:utf-8 -*- li=[11,22,33,44,55] r= filter(lambda x:x>22,li) prin ...

  9. 19-[模块]-xml

    1.xml协议 xml是实现不同语言或程序之间进行数据交换的协议,跟json差不多,但json使用起来更简单,不过,古时候,在json还没诞生的黑暗年代,大家只能选择用xml呀,至今很多传统公司如金融 ...

  10. python实战——教你用微信每天给女朋友说晚安

    但凡一件事,稍微有些重复.我就考虑怎么样用程序来实现它. 这里给各位程序员朋友分享如何每天给朋友定时微信发送”晚安“,故事,新闻,等等··· ··· 最好运行在服务器上,这样后台挂起来更方便. 准备 ...