寻找数组中第k大的元素

寻找数组中第k大的元素

该文章转自https://blog.csdn.net/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,就是个不好的方法。

该文章转自https://blog.csdn.net/qq_26286193/article/details/80683004,这篇文章只是对原文章的自身的一个梳理,见谅。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值