Lettcode Kth Largest Element in an Array
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的更多相关文章
- 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 ...
- leetcode面试准备:Kth Largest Element in an Array
leetcode面试准备:Kth Largest Element in an Array 1 题目 Find the kth largest element in an unsorted array. ...
- 【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 ...
- 剑指offer 最小的k个数 、 leetcode 215. Kth Largest Element in an Array 、295. Find Median from Data Stream(剑指 数据流中位数)
注意multiset的一个bug: multiset带一个参数的erase函数原型有两种.一是传递一个元素值,如上面例子代码中,这时候删除的是集合中所有值等于输入值的元素,并且返回删除的元素个数:另外 ...
- 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 ...
- [Leetcode Week11]Kth Largest Element in an Array
Kth Largest Element in an Array 题解 题目来源:https://leetcode.com/problems/kth-largest-element-in-an-arra ...
- 网易2016 实习研发工程师 [编程题]寻找第K大 and leetcode 215. Kth Largest Element in an Array
传送门 有一个整数数组,请你根据快速排序的思路,找出数组中第K大的数. 给定一个整数数组a,同时给定它的大小n和要找的K(K在1到n之间),请返回第K大的数,保证答案存在. 测试样例: [1,3,5, ...
- 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 ...
- 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 ...
随机推荐
- Java高并发之同步异步
1.概念理解: 2.同步的解决方案: 1).基于代码 synchronized 关键字 修饰普通方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁. 修饰静态方法:作用于当前类对象加锁,进入同 ...
- 通信服务器哈希Socket查找(Delphi)
在Socket通信服务器的开发中,我们经常会需要Socket与某个结构体指针进行绑定.当连接量很大时,意味着需要个高效的查找方法 Delphi中提供了哈希算法类,以此类为基础,修改出Socket专用M ...
- 对文件 I/O,标准 I/O 的缓冲的理解
1.标准I/O缓冲区 要理解标准I/O,就要先知道文件I/O的业务逻辑. 下面图示为文件I/O 如执行下面的代码: write(fd, buf2, sizeof(buf2)); 图中 buf:就是bu ...
- Android 自定义WebView 实现可以加载缓存数据
1.自定义WebView说明 1.1.这个WebView可以加载缓存的数据.(需要后端配合,将html转换成一个字符串,主要是图片要用特殊格式) 1.2.注入了图片链接,为了方便点击webView中的 ...
- PHP.35-TP框架商城应用实例-后台11-商品分类-删除分类(2种方法)、添加、修改
删除分类 删除一个分类的同时,其所有子分类都删除 在控制器CategoryCtroller.class.php中添加删除函数(delete) 在分类模型中添加钩子函数_before_delete()[ ...
- [转]渗透测试tips...零碎的小知识
1.拿到一个待检测的站,你觉得应该先做什么?收集信息whois.网站源IP.旁站.C段网站.服务器系统版本.容器版本.程序版本.数据库类型.二级域名.防火墙.维护者信息另说… 2.mysql的网站注入 ...
- Java集合——LinkedHashMap源码详解
个KV.LinkedHashMap不仅像HashMap那样对其进行基于哈希表和单链表的Entry数组+ next链表的存储方式,而且还结合了LinkedList的优点,为每个Entry节点增加了前驱和 ...
- 微信小程序 | 49,小程序入门集锦系列文章20篇
以下20篇文章,都是关于微信小程序的文章,以入门常见问题为主.如发现谬误,请与笔者联系. [小程序入门集锦]1,微信小程序在哪里打开 [小程序入门集锦]2,小程序商店 [小程序入门集锦]3,微信小程序 ...
- Toolbar中menu菜单文字颜色的修改
Toolbar菜单中menu当中我们大多数都使用图片来按钮,可是有些时候我们也会直接使用文字,文字的颜色如何修改呢. 其实很简单,我们只要修改styles.xml文件中,添加一句 <item n ...
- python的列表生成式和生成器
1.列表生成式是Python受欢迎的语法之一,通过一句简洁的语法就可以对一组元素进行过滤,还可以对得到的元素进行转换处理,语法格式为: [exp for val in collection if co ...