寻找数组中第k大的元素
该文章转自https://blog.youkuaiyun.com/qq_26286193/article/details/80683004,这篇文章只是对原文章的自身的一个梳理,见谅。
方法1:直接对数组进行排序然后查找
最简单直接的方法是对数组进行排序(排序算法可以根据数据来选择),排序之后利用数组下表取出数组中第 k 大的元素。 假设使用快速排序,则时间复杂度为 O(NlogN)+O(k)=O(NlogN),但是这中方法时间复杂度较高,浪费了较多时间,因为题目只是需要求出第 k 大的元素,而不需要对整个数组的数据进行排序。
方法2:部分元素排序
当 k 较小时,利用 k 趟排序是个较优良的方法,因为我们只需要排序 k 趟就能将 第 k 大的元素放到指定的位置,例如可以利用 冒泡排序 只进行 k 趟起泡排序(外循环中设置循环边界为k),或者利用选择排序 只进行 k 趟选择(外循环设置循环边界为k),都能有效地使 第 k 大的元素放到指定的位置。
//只进行k趟起泡得到第k大的元素
int find_k_max(int arr[],int n,int k){
//只进行k趟起泡
if(k==n)
boundary=n-1;
else
boundary=k;
bool flag; //判断在某一趟起泡中是否发生了交换,如果没有发生交换,则说明已经完成了排序
for(int i=0;i<boundary;i++){ //设置循环边界为k,只进行k趟起泡
flag=false;
for(int j=0;j<n-i-1;j++){
if(arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
flag=true;
}
}
if(!flag) //如果某一趟起泡中没有发生起泡,说明已经不需要再起泡了
break;
}
return arr[n-k];
}
利用 只进行k趟排序来求数组中·第k大的元素的方法,时间复杂度为O(n*k), 但该方法适用于 k 比较小的情况,否则时间复杂度较高,与直接进行冒泡排序相似。
方法3:快排的分治法,其实这个方法是一种普遍可以使用的选择算法,期望运行时间为O(N)
因为我们只需要求数组中第 k 大的元素,利用快速排序的思想,第 k 大的元素在成为某一次快排中的主元时,其右边(这里的右边是说在整个数组中,而非是分治的区域中的)比它大的元素的个数就是 k-1 个(第k大元素,所以只有k-1个元素比它更大),故可以利用这个性质来求数组中的第 k 大元素。
//利用快速排序的分治思想求数组中第k大的元素
int partition(int arr[],int low,int high){
//选择主元的方法有多种——随机化选择,固定选择,三元取中等
//此处选择固定选择
int temp=arr[low];
while(low<high){
while(low<high&&arr[high]>temp)
high--;
arr[low]=arr[high];
while(low<high&&arr[low]<=temp)
low++;
arr[high]=arr[low];
}
arr[low]=temp;
return low; //low就是主元的位置
}
int find_k_max_1(int arr[],int low,int high,int k){
int mid=partition(arr,low,high);
//求出包括arr[mid]的右半边的长度
int r_length=high-mid+1;
if(r_length==k)
return arr[mid];
else if(r_length>k){
//右半边长度比k大,说明第k大元素还在右半边,需要继续在右半边进行查找
return find_k_max_1(arr,mid+1,high,k);
}
else{
//右半边长度小于k,说明第k大元素在左边,需要在左边进行查找
return find_k_max_1(arr,low,mid-1,k-r_length); //这里注意在左边查找时需要修改传递的k值,因为传递的k值是给分治的数组中使用的
}
}
使用 快速排序的分治思想来求第k大的元素,时间复杂度为O(Nlogk),但当每次分区都是极不平凡的情况,时间复杂度就会退化为O(Nk)
方法4:借助有限队列
struct cmp
{
bool operator()(int &a, int &b) const
{
//因为优先出列判定为!cmp,所以反向定义实现最小值优先
return a > b;
}
};
int findMaxK(int a[], int n, int k) {
priority_queue<int,vector<int>,cmp> myqueue;
for (int i = 0; i < n; i++) {
if (myqueue.size() < k) {
myqueue.push(a[i]);
}
else {
//将最小元素与a[i]比较
int min = myqueue.top();
if (a[i] > min) {
myqueue.pop();
}
myqueue.push(a[i]);
}
}
return myqueue.top();
}
时间复杂度为O(N*logk),因为二叉堆的插入和删除操作都是 logk的时间复杂度,并且该方法不处理重复数据,如果要求去除重复数据,使用平衡二叉搜索树会更加合适。STL标准库中提供的红黑树实现的set可以解决去重的问题。
方法5:键值索引法
采用计数排序的思想,将数组中的每个元素出现的次数记录下来,然后进行一定的运算就可以得到第k的的元素。这个方法对原数组的数据有要求:所有数据都是正整数,并且这些数据都是在 [0 - max]的一个范围,且 max 的取值不大(即数据的取值范围不大)。此时可以申请·一个规模为 max+1的数组coount,将count的数组全部初始化为0,然后利用count记录数组中每个元素出现的个数,这样就可以在O(N)时间复杂度下找到数组中的第 k 大元素。
// 利用计数排序的思想,将数组中的每个元素出现的次数记录下来,然后进行一定的运算就可以得到第k的的元素。
int find_k_max2(int arr[],int n, int k,int max){
//要求数组中的元素都是正整数,且都在[0,max]的范围
int count[max+1];
for(int i=0;i<max;i++)
count[i]=0;
for(int j=0;j<n;j++){
count[arr[j]]=count[arr[j]]+1; //记录arr数组中每个元素出现的次数
}
int index=max;
while(k>0){
//从count数组的末尾开始循环,即从大到小查找
//如果count[index]不为0,且还未找到第k大元素,说明count[index]是比k大的元素出现的次数,每次循环count[index]--,k--,就意味着少了一个比k大的元素;
//而如果count[index]为0,则说明这个元素不存在,直接index--,进入下个循环判断下一个元素
if(count[index]==0)
index--;
else{
count[index]--;
k--;
}
}
}
你可能会觉得,这种方法实在是好,只需O(N)的时间复杂度。但仔细想想,上面代码实现的键值索引法并不一定是O(N)的时间复杂度。确实,一开始构建count数组时,只需访问原数组a一次即可,时间为O(N),但count构建结束后,从count中找到第k大的元素却并不是O(N)时间。大家不妨考虑一种情况:如果max比N大得多呢?比如要找a={1,2,3,100,100}的第3大元素,首先构建一个长度为101的数组,得把数组的全部元素置为0,O(max)。然后用count统计a中每个整数出现的次数,O(N)。最后寻找第3大元素,cout的访问次数为接近100次。因此,这个方法实际的时间复杂度为O(max)。
结论:这种方法的时间复杂度为O(max),当max与N接近时,才能获得较好的性能。若max远大于N,就是个不好的方法。