如何寻找无序数组中的第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. P1006 传纸条

    题目描述 小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题.一次素质拓展活动中,班上同学安排做成一个m行n列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了.幸运的是 ...

  2. UVA - 11927 Games Are Important (SG)

    Description  Games Are Important  One of the primary hobbies (and research topics!) among Computing ...

  3. oracle 事务 数据伪列

    在用户进行数据更新操作(DML)事务一定会起作用. 事务的出现会保证数据的完整性.一致性.在整个事务的处理过程之中主要使用两个操作命令: · 事务的提交(COMMIT):是真正的向数据库之中发出更新指 ...

  4. 第一次作业:基于Linux 4.5的进程模型与调度器分析

    1.操作系统是怎么组织进程的? 1.1什么是线程,什么是进程: 刚接触时可能经常会将这两个东西搞混.简单一点的说,进程是一个大工程,线程则是这个大工程中每个小地方需要做的东西(在linux下看作&qu ...

  5. Vmware 下安装linux虚拟机

    由于想自己玩玩linux系统,就想着装一个linux的虚拟机,虚拟机vmware很好找,也很好用,但是linux镜像安装老是出问题,然后就找了很多版本的,最后实验成功一种,在这里分享给大家. 一.安装 ...

  6. Dynamics 365 可编辑子网格的字段禁用不可编辑

    在365中引入了subgrid的行可编辑,那随之带来的一个问题就是,在主表单禁用的状态下,如何禁用行编辑呢,这里就用到了subgrid的OnRecordSelect方法. 代码很简单,   我这里是禁 ...

  7. 2018-11-26 BIG DATA ANALYSIS

  8. Flex自定义组件开发 - jackyWHJ

    一般情况下需要组件重写都是由于以下2个原因: 1.在FLEX已有组件无法满足业务需求,或是需要更改其可视化外观等特性时,直接进行继承扩展. 2.为了模块化设计或进一步重用,需要对FLEX组件进行组合. ...

  9. 20155222卢梓杰 课堂测试ch06补做

    20155222卢梓杰 课堂测试ch06补做 1.下面代码中,对数组x填充后,采用直接映射高速缓存,所有对x和y引用的命中率为() A . 1 B . 1/4 C . 1/2 D . 3/4 正确答案 ...

  10. 《记忆匣子》——网络编程jsp网页制作复习习笔记1

    内容都在图里 哈哈哈 <%@ page language="java" contentType="text/html; charset=utf-8" pa ...