【算法】寻找第K大的数(快排思想实现二分)

  • Post author:
  • Post category:其他



寻找第K大数




思考题



给定N个数,求其中第K大的数。


题目传送门

查找第K大 CSU 2078

小W有很强的好胜心,也有很明确的目标,总是希望当第k名,但是小W太菜了,经常达不到目标,于是他每次考试后都想知道第k名的分数是多少,然后以它为目标。 现在给出了每个人的分数,请求编程能力很强的你帮他迅速找到第k名的分数为多少,这样他才有更多的时间去学习。

第一行为一个正整数t代表有t组数据。每组数据第一行为两个正整数n和k,第二行为n个正整数。 1 <= k <= n <=10^7

对于每组数据,输出第k大的数

input

1
6 2
1 2 3 4 5 6

output

5




方法1:快排中的二分思想



时间复杂度时O(n), 大概最快的解决方法了。

找一个基准数,把比它大的放在左边,比它小的放在右边。如果最终该数数所在 位置刚好是第k个,那么它就是答案。否则只需要在该数组的左半部分找,或者右半部分找。

实现方法就是利用快速排序的原理,以此达到二分查找的目的。另外由于此题数量级比较高1e7,输入必须要用“读入挂”。代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

///读入挂(大量数据时,比scanf都快好几倍)
namespace fastIO
{
	#define BUF_SIZE 100000
	bool IOerror = 0;
	inline char nc()
	{
		static char buf[BUF_SIZE], *p1 = buf + BUF_SIZE, *pend = buf + BUF_SIZE;
		 	if ( p1 == pend )
			 {
			  		p1	= buf;
					pend	= buf + fread( buf, 1, BUF_SIZE, stdin );
					if ( pend == p1 )
					{
							IOerror = 1;
							return(-1);
					}
			}

			return(*p1++);

	}
	inline bool blank( char ch )
	{
		return(ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t');
	}
	inline void rd( int &x )
	{
		char ch;
		while ( blank( ch = nc() ) )
		;
		if ( IOerror )
		return;
		for ( x = ch - '0'; (ch = nc() ) >= '0' && ch <= '9'; x = x * 10 + ch - '0' )
		;
	}
	#undef BUF_SIZE
};
using namespace fastIO;
/**
以上是copy的读入挂代码,codeblocks可能运行不会报错,但是不支持,但是 oj 上面提交妥妥的。
只能用来输入整数,使用方法是
	int a;
	rd(a);
**/
using namespace std;
const int N = 1e7 + 5;
int a[N];
int main()
{
    int t;
    rd(t);
    while(t--){
        int n , k;
        rd(n); rd(k);
        for(int i = 0; i < n; i++){
            rd(a[i]);
        }
        //拟快排
        for(int l = 0 , r = n-1; l <= r; ){///样例:1 \n 5 3 \n 1 2 3 4 5 这种情况,最终i,j相等的时候,也需要进行一次循环。
            int i = l, j = r;
            int temp = a[l];
            while(i < j){
                while(i < j && a[j] <= temp) j--; a[i] = a[j];//此处以a[i]为中间变量将a[j] , a[i]两者值进行了交换。
                while(i < j && a[i] >= temp) i++; a[j] = a[i];
            }
            a[i] = temp;//基准数移到中间位置。实际上此时a[l]已经被改变,但是对此题目来说不影响结果。
            if(i == k-1){printf("%d\n", a[k-1]); break;}//第K大的数,下标为k-1
            else if(k-1 > i)l = i + 1;
            else if(k-1 < i)r = i - 1;
        }
    }
    return 0;
}
/**
耗时:888ms < 1s
Memory:41184 Kb
Accepted

codeblocks 不支持读入挂 , 但 oj 支持。
**/




方法2:直接排序(肯定是超时妥妥的)


//经典冒泡排序
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e5;
int a[N];
int main()
{
    int n, k;
    scanf("%d%d", &n, &k);
    for(int i = 0; i<n; i++){
        scanf("%d", &a[i]);
    }
    //冒泡排序由大到小
    for(int i = 0; i < n; i++){  		//需要冒泡的次数
        bool p = false;					//加入布尔变量判断是否还需要继续比较
        for(int j = 0; j < n-i-1; j++){ //进行冒泡,每个数和后一个数比较
            if(a[j] < a[j+1]){
                int t;
                t = a[j], a[j] = a[j+1], a[j+1] = t;
                p = true;
            }
        }
        if(!p) break;
    }
    printf("第%d大的数为%d", k, a[k-1]);
    return 0;
}


冒泡缺点很明显, 效率很低。

下面试一下直接快速排序+读入挂,然后输出第K大数。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <algorithm>
namespace fastIO
{
	#define BUF_SIZE 100000
	bool IOerror = 0;
	inline char nc()
	{
		static char buf[BUF_SIZE], *p1 = buf + BUF_SIZE, *pend = buf + BUF_SIZE;
		 	if ( p1 == pend )
			 {
			  		p1	= buf;
					pend	= buf + fread( buf, 1, BUF_SIZE, stdin );
					if ( pend == p1 )
					{
							IOerror = 1;
							return(-1);
					}
			}

			return(*p1++);

	}
	inline bool blank( char ch )
	{
		return(ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t');
	}
	inline void rd( int &x )
	{
		char ch;
		while ( blank( ch = nc() ) )
		;
		if ( IOerror )
		return;
		for ( x = ch - '0'; (ch = nc() ) >= '0' && ch <= '9'; x = x * 10 + ch - '0' )
		;
	}
	#undef BUF_SIZE
};
using namespace fastIO;

using namespace std;
const int N = 1e7+5;
int a[N];
int main()
{
    int t;
    rd(t);
    while(t--){
        int n , k;
        rd(n); rd(k);
        for(int i = 0; i < n; i++){
            rd(a[i]);
        }
        sort(a , a+n);
        printf("%d\n", a[n-k]);
    }
    return 0;
}
/**
直接快速排序,数量级太高
数量级1e7 , 时限1000ms

实际用时>2060ms(超出最大限制)
空间复杂度O(n)
**/





方法3:multiset容器的使用(依旧差了点,暴时)



思路:维护最大的K个数。先把前K个数读入数组,并且进行排序。然后,将剩下的元素在逐个读入。当新的元素被读到时,如果他小于数组的最小的元素则忽略,否则先删除数组中最小的元素,然后再将该元素放在数组的正确位置。


该方法需要做大量的排序、查询、删除、插入的操作,因此很容易想到使用集合set代替上面提到的数组, 但是由于set具有确定性,其中的元素不可重合。



有一种既有set的有序性,又可以存储可重复元素的容器——multiset。使用方法和set基本一致

#include <cstdio>
#include <cstring>
#include <iostream>
#include <set>
#include <algorithm>
namespace fastIO
{
	#define BUF_SIZE 100000
	bool IOerror = 0;
	inline char nc()
	{
		static char buf[BUF_SIZE], *p1 = buf + BUF_SIZE, *pend = buf + BUF_SIZE;
		 	if ( p1 == pend )
			 {
			  		p1	= buf;
					pend	= buf + fread( buf, 1, BUF_SIZE, stdin );
					if ( pend == p1 )
					{
							IOerror = 1;
							return(-1);
					}
			}

			return(*p1++);

	}
	inline bool blank( char ch )
	{
		return(ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t');
	}
	inline void rd( int &x )
	{
		char ch;
		while ( blank( ch = nc() ) )
		;
		if ( IOerror )
		return;
		for ( x = ch - '0'; (ch = nc() ) >= '0' && ch <= '9'; x = x * 10 + ch - '0' )
		;
	}
	#undef BUF_SIZE
};
using namespace fastIO;
using namespace std;
//可重复集合
multiset<int> a;
int main()
{
    int t;
    rd(t);
    while(t--){
    int n , k;
    rd(n); rd(k);
    for(int i = 0; i < n; i++){
        int x;
        rd(x);
        if(a.size() < k)
            a.insert(x);
        else if(x > *(a.begin())){
            a.erase(a.begin());
            a.insert(x);
        }
        else
            continue;
    }
    int i = k;
    for(multiset<int>::iterator it = a.end(); i >= 0; it--, i--){
        if(i == 0)
            printf("%d\n", *it);
    }
    }
    return 0;
}
/**
比直接用快排快了点,不过还是超时
用时:1998ms , 内存占用大:76000kb
另外:本地codeblocks 不能使用读入挂,似乎只有在oj上才能用。
**/


这种方法比直接快速排序快一点,但是还是不行。



版权声明:本文为qq_42848174原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。