Lettcode Kth Largest Element in an Array

题意:在无序数组中,寻找第k大的数字,注意这里考虑是重复的。

一直只会简单的O(nlogn)的做法,听说这题有O(n)的算法,于是赶紧找了个博客学习了一波,受益匪浅啊。

以下内容出处http://www.cnblogs.com/informatics/p/5092741.html

查找第K小的数 BFPRT算法

BFPRT算法是解决从n个数中选择第k大或第k小的数这个经典问题的著名算法,但很多人并不了解其细节。本文将首先介绍求解这个第k小数字问题的几个思路,然后重点介绍在最坏情况下复杂度仍然为O(n)的BFPRT算法。

一 基本思路

关于选择第k小的数有许多方法

将n个数排序(比如快速排序或归并排序),选取排序后的第k个数,时间复杂度为O(nlogn)。

维护一个k个元素的最大堆,存储当前遇到的最小的k个数,时间复杂度为O(nlogk)。这种方法同样适用于海量数据的处理。

部分的快速排序(快速选择算法),每次划分之后判断第k个数在左右哪个部分,然后递归对应的部分,平均时间复杂度为O(n)。但最坏情况下复杂度为O(n^2)。

BFPRT算法,修改快速选择算法的主元选取规则,使用中位数的中位数的作为主元,最坏情况下时间复杂度为O(n)。

二 快速选择算法

快速选择算法就是修改之后的快速排序算法,前面快速排序的实现与应用这篇文章中讲了它的原理和实现。

其主要思想就是在快速排序中得到划分结果之后,判断要求的第k个数是在划分结果的左边还是右边,然后只处理对应的那一部分,从而达到降低复杂度的效果。

在快速排序中,平均情况下数组被划分成相等的两部分,则时间复杂度为T(n)=2*T(n/2)+O(n),可以解得T(n)=nlogn。

在快速选择中,平均情况下数组也是非常相等的两部分,但是只处理其中一部分,于是T(n)=T(n/2)+O(n),可以解得T(n)=O(n)。

但是两者在最坏情况下的时间复杂度均为O(n^2),出现在每次划分之后左右总有一边为空的情况下。为了避免这个问题,需要谨慎地选取划分的主元,一般的方法有:

固定选择首元素或尾元素作为主元。

随机选择一个元素作为主元。

三数取中,选择三个数的中位数作为主元。一般是首尾数,再加中间的一个数或者随机的一个数。

为了方便,这里把前面的代码也放在这里。

int partition(int a[], int l, int r) //对数组a下标从l到r的元素进行划分
{
//随机选取一个数作为划分的基数
int rd = l + rand() % (r-l+1);
swap(a[rd], a[r]); int j = l - 1; //左边数字最右的下标
for (int i = l; i < r; i++)
if (a[i] <= a[r])
swap(a[++j], a[i]);
swap(a[++j], a[r]);
return j;
}
int NthElement(int a[], int l, int r, int id) //求数组a下标l到r中的第id个数
{
if (l == r) return a[l]; //只有一个数
int m = partition(a, l, r), cur = m - l + 1;
if (id == cur) return a[m]; //刚好是第id个数
else if(id < cur) return NthElement(a, l, m-1, id);//第id个数在左边
else return(a, m+1, r, id-cur); //第id个数在右边
}

三 BFPRT算法

BFPRT算法,又称为中位数的中位数算法,由5位大牛(Blum 、 Floyd 、 Pratt 、 Rivest 、 Tarjan)提出,并以他们的名字命名。参考维基上的介绍Median of medians

算法的思想是修改快速选择算法的主元选取方法,提高算法在最坏情况下的时间复杂度。其主要步骤为:

首先把数组按5个数为一组进行分组,最后不足5个的忽略。对每组数进行排序(如插入排序)求取其中位数。

把上一步的所有中位数移到数组的前面,对这些中位数递归调用BFPRT算法求得他们的中位数。

将上一步得到的中位数作为划分的主元进行整个数组的划分。

判断第k个数在划分结果的左边、右边还是恰好是划分结果本身,前两者递归处理,后者直接返回答案。

首先看算法的主程序,代码如下。小于5个数的情况直接处理返回答案。否则每5个进行求取中位数并放到数组前面,递归调用自身求取中位数的中位数,然后用中位数作为主元进行划分。

注意这里只利用了中位数的下标,而不关心中位数的数值,目的是方便在划分函数中使用下标直接进行交换。BFPRT算法执行完毕之后可以保证我们想要的数字是排在了它真实的位置上,所以可以直接使用中位数的下标。

int BFPRT(int a[], int l, int r, int id) //求数组a下标l到r中的第id个数
{
if (r - l + 1 <= 5) //小于等于5个数,直接排序得到结果
{
insertionSort(a, l, r); return a[l + id - 1];
} int t = l - 1; //当前替换到前面的中位数的下标
for (int st = l, ed; (ed = st + 4) <= r; st += 5) //每5个进行处理
{
insertionSort(a, st, ed); //5个数的排序
t++; swap(a[t], a[st+2]); //将中位数替换到数组前面,便于递归求取中位数的中位数
} int pivotId = (l + t) >> 1; //l到t的中位数的下标,作为主元的下标
BFPRT(a, l, t, pivotId-l+1);//不关心中位数的值,保证中位数在正确的位置
int m = partition(a, l, r, pivotId), cur = m - l + 1;
if (id == cur) return a[m]; //刚好是第id个数
else if(id < cur) return BFPRT(a, l, m-1, id);//第id个数在左边
else return BFPRT(a, m+1, r, id-cur); //第id个数在右边
}

这里的划分函数与之前稍微不同,因为指定了划分主元的下标,所以参数增加了一个,并且第一步需要交换主元的位置。代码如下:

int partition(int a[], int l, int r, int pivotId) //对数组a下标从l到r的元素进行划分
{
//以pivotId所在元素为划分主元
swap(a[pivotId],a[r]); int j = l - 1; //左边数字最右的下标
for (int i = l; i < r; i++)
if (a[i] <= a[r])
swap(a[++j], a[i]);
swap(a[++j], a[r]);
return j;
}

这里简单分析一下BFPRT算法的复杂度。

划分时以5个元素为一组求取中位数,共得到n/5个中位数,再递归求取中位数,复杂度为T(n/5)。

得到的中位数x作为主元进行划分,在n/5个中位数中,主元x大于其中1/2n/5=n/10的中位数,而每个中位数在其本来的5个数的小组中又大于或等于其中的3个数,所以主元x至少大于所有数中的n/103=3/10n个。同理,主元x至少小于所有数中的3/10n个。即划分之后,任意一边的长度至少为3/10,在最坏情况下,每次选择都选到了7/10的那一部分,则递归的复杂度为T(7/10*n)。

在每5个数求中位数和划分的函数中,进行若干个次线性的扫描,其时间复杂度为cn,其中c为常数。其总的时间复杂度满足 T(n) <= T(n/5) + T(7/10n) + c * n。

我们假设T(n)=xn,其中x不一定是常数(比如x可以为n的倍数,则对应的T(n)=O(n^2))。则有 xn <= xn/5 + x7/10n + cn,得到 x<=10c。于是可以知道x与n无关,T(n)<=10c*n,为线性时间复杂度算法。而这又是最坏情况下的分析,故BFPRT可以在最坏情况下以线性时间求得n个数中的第k个数。

算法复杂度也可以用树的方式来较准确的估计(略)

算法的关键在于将中位数的中位数作为基准有效的减少了划分的次数,至于为什么是选5个作为一组,而不是3,7,9等等,wiki上有分析。

class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
return findKthSmallest(0, nums.size() - 1, nums, nums.size() - k + 1);
}
int findKthSmallest(int l, int r, vector<int>& nums, int k){
if(r - l < 5){
sort(nums.begin()+l,nums.begin()+r+1);
return nums[l + k - 1];
}
int t = l - 1;
for(int st = l;st + 4 <= r;st+=5){
sort(nums.begin()+st,nums.begin()+st+5);
swap(nums[++t],nums[st+2]);
}
int pivo = (l + t) >> 1;
findKthSmallest(l, t, nums, pivo - l + 1);
int m = partitionn(l, r, pivo, nums);
if(k == m - l + 1) return nums[m];
if(k < m - l + 1) return findKthSmallest(l, m, nums, k);
return findKthSmallest(m+1, r, nums, k - m + l - 1);
}
int partitionn(int l,int r,int pivo,vector<int>& nums){
///以pivo所在元素为划分主元
swap(nums[pivo], nums[r]);
int j = l - 1;
for(int i = l;i < r;i++)
if(nums[i] < nums[r]) swap(nums[++j],nums[i]);
swap(nums[++j],nums[r]);
return j;
} };

等我写完这道题,去看最优代码,两行丢给我一个std::nth_element,厉害了。原来c++库早已经封装好了找第k小的函数,我猜内部实现和上述算法应该差不多了。

来看看具体用法:

template<class _RanIt, class _Pr> inline
void nth_element(_RanIt _First, _RanIt _Nth, _RanIt _Last, _Pr _Pred) template<class _RanIt> inline
void nth_element(_RanIt _First, _RanIt _Nth, _RanIt _Last) 调用nth_element返回的序列,Nth前面的数都不大于它,后面的都不小于它,但是前面的区间和后面的区间并不是有序的,也就是说第Nth个数就是Nth小的数,第一种方法的_Pred 类似cmp,自定义排序规则。
举个例子:
///求区间[l,r) 第k小
std::nth_element(tmp.begin()+l,tmp.begin()+l+k-1,tmp.begin()+r);
cout<<tmp[l+k-1]<<endl;

Lettcode Kth Largest Element in an Array的更多相关文章

  1. Kth Largest Element in an Array

    Find K-th largest element in an array. Notice You can swap elements in the array Example In array [9 ...

  2. leetcode面试准备:Kth Largest Element in an Array

    leetcode面试准备:Kth Largest Element in an Array 1 题目 Find the kth largest element in an unsorted array. ...

  3. 【LeetCode】215. Kth Largest Element in an Array (2 solutions)

    Kth Largest Element in an Array Find the kth largest element in an unsorted array. Note that it is t ...

  4. 剑指offer 最小的k个数 、 leetcode 215. Kth Largest Element in an Array 、295. Find Median from Data Stream(剑指 数据流中位数)

    注意multiset的一个bug: multiset带一个参数的erase函数原型有两种.一是传递一个元素值,如上面例子代码中,这时候删除的是集合中所有值等于输入值的元素,并且返回删除的元素个数:另外 ...

  5. Leetcode 之 Kth Largest Element in an Array

    636.Kth Largest Element in an Array 1.Problem Find the kth largest element in an unsorted array. Not ...

  6. [Leetcode Week11]Kth Largest Element in an Array

    Kth Largest Element in an Array 题解 题目来源:https://leetcode.com/problems/kth-largest-element-in-an-arra ...

  7. 网易2016 实习研发工程师 [编程题]寻找第K大 and leetcode 215. Kth Largest Element in an Array

    传送门 有一个整数数组,请你根据快速排序的思路,找出数组中第K大的数. 给定一个整数数组a,同时给定它的大小n和要找的K(K在1到n之间),请返回第K大的数,保证答案存在. 测试样例: [1,3,5, ...

  8. LN : leetcode 215 Kth Largest Element in an Array

    lc 215 Kth Largest Element in an Array 215 Kth Largest Element in an Array Find the kth largest elem ...

  9. LeetCode OJ 215. Kth Largest Element in an Array 堆排序求解

    题目链接:https://leetcode.com/problems/kth-largest-element-in-an-array/ 215. Kth Largest Element in an A ...

随机推荐

  1. scoped,会使设置UI组件库的样式识别不出来

    未设置 scoped 作用域:显示效果 设置作用域的效果:ui组件默认的值(你怎么设置都不管用)

  2. 洛谷P1762 偶数(找规律)

    题目描述 给定一个正整数n,请输出杨辉三角形前n行的偶数个数对1000003取模后的结果. 输入输出格式 输入格式: 一个数 输出格式: 结果 输入输出样例 输入样例#1: 复制 6 输出样例#1:  ...

  3. 【赛时总结】 ◇赛时·III◇ AtCoder ABC-099

    ◆赛时·III◆ ABC-099 ■唠叨■ 不要问我为什么先给ABC-100写了博客再写的ABC-099-- 莫名觉得这次比赛特别简单--虽然我并没有参加比赛,只是之后再补做的.QwQ ■试题& ...

  4. 【MySQL】使用SQL语句操作MySQL

    前言: MySQL在工作中是最常用的数据库,但在使用Django和Flask的时候,都是使用ORM进行操作,除了select语句外,其他的SQL语句操作MySQL的能力没有啥提高,为了解决这个问题, ...

  5. python实现简单分类knn算法

    原理:计算当前点(无label,一般为测试集)和其他每个点(有label,一般为训练集)的距离并升序排序,选取k个最小距离的点,根据这k个点对应的类别进行投票,票数最多的类别的即为该点所对应的类别.代 ...

  6. tp5.0初入

    1.目录结构 |-application 应用目录 是整个网站的核心 |---|---index 前台目录 |---|-----|---controller 控制器 |---|-----|---mod ...

  7. Drazil and Tiles CodeForces - 516B (类拓扑)

    Drazil created a following problem about putting 1 × 2 tiles into an n × m grid: "There is a gr ...

  8. 我理解中的Hadoop HDFS分布式文件系统

    一,什么是分布式文件系统,分布式文件系统能干什么 在学习一个文件系统时,首先我先想到的是,学习它能为我们提供什么样的服务,它的价值在哪里,为什么要去学它.以这样的方式去理解它之后在日后的深入学习中才能 ...

  9. mysql in和exists性能比较和使用【转】

    exists对外表用loop逐条查询,每次查询都会查看exists的条件语句,当 exists里的条件语句能够返回记录行时(无论记录行是的多少,只要能返回),条件就为真,返回当前loop到的这条记录, ...

  10. Java从后台重定向(redirect)到另一个项目的方法

    (1)通过ModelAndView跳转 @RequestMapping("alipayforward") public ModelAndView alipayforward(Htt ...