P1923 【深基9.例4】求第 k 小的数
1.题目
【深基9.例4】求第 k 小的数
题目描述
输入 \(n\)(\(1 \le n < 5000000\) 且 \(n\) 为奇数)个数字 \(a_i\)(\(1 \le a_i < {10}^9\)),输出这些数字的第 \(k\) 小的数。最小的数是第 \(0\) 小。
请尽量不要使用 nth_element
来写本题,因为本题的重点在于练习分治算法。
输入格式
输出格式
样例 #1
样例输入 #1
5 1
4 3 2 1 5
样例输出 #1
2
2.题解
2.1 nth_element函数
思路
在强大的STL库中存在一个神奇的函数,那就是nth_element,这个函数主要用来将数组元素中第k小的整数排出来并在数组中就位,随时调用,可谓十分实用。
他的时间复杂的为O(n), 原因是他不要像排序那样确定每个元素都在自己应该在的位置上,它只要保证指定的k位置左边元素均小于它,右边元素均大于它即可。
故实际实现使用分治算法思想,只需要首尾指针对于数组进行一次递推遍历即可,这里先讲nth_element函数的使用方法,在2.2中详细介绍实现。
函数语句:nth_element(数组名,数组名+第k小元素,数组名+元素个数)
代码
#include<bits/stdc++.h>
using namespace std;
long long n,k,a[5000010];
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
nth_element(a,a+k,a+n);//使第k小整数就位
printf("%d",a[k]);//调用第k小整数
}
2.2 分治算法-nth_element函数的具体实现
1.思路
我们使用分治思想,每次将整个区间分为左右两个区域,选取一个基准点后,通过首尾指针不断逼近,中间通过交换保证r左侧的区间全部小于等于currNum, l右侧的区间全部大于等于currNim
直到最后l > r结束循环,此时已经可以保证[begin, r] 和 [l, end]两个区域都是"有序的",也就是左边均小于等于currNum,右边均大于等于currNum
我们再来判断目标位置k所处的位置,分三种情况,每次都可以二分缩小一半查找范围:
1.k <= r, 去左半区间[begin, r]继续寻找
2.k >= l, 去右半区间[l, end]继续寻找
3.r < k < l. 这种情况比较特殊,具体可以见情况二图片,必然是 l - r = 2, k = r + 1 = l - 1的情况,
否则如果像情况一,l - r = 1, 那么必然有 k <= r 或者 k >= l, 而不可能存在r < k < l
大致的情况有以下两种:
情况一:
情况二:
2.代码
#include<bits/stdc++.h>
using namespace std;
int k, ans;
//这里arr必须设置为全局变量,不然每次都作为函数输入参数,会消耗大量资源和时间爱你
vector<int> arr;
// 这里由于cin读入过慢,而这里n( 1≤<5000000且n为奇数),所以使用快读缩短读取时间爱
inline int read(){ //快读
char ch=getchar();
int x=0,f=1;
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while('0'<=ch&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
} return x*f;
}
void sortNum(int begin, int end){
// 递归终止条件
if(begin == end){
ans = arr[begin];
return;
}
// 初始化
int l = begin, r = end;
// 确定一个基准判断点, 只固定值,不固定位置!!!
int currNum = arr[(end - begin) / 2 + begin];
while(l <= r){
//由于我取的currNum是中位数,所以l第一次最多停留在中位数,r同理
//由于后面if中的交换,l指针左边的左半区间必都是小于等于currNum的值,r指针右边的右半区间必是大于等于currNum的值,都是一种"有序"状态
while(arr[l] < currNum) l++; //l指向第一个不小于currNum的值
while(arr[r] > currNum) r--; //r指向第一个不大于currNum的值
if(l <= r){
swap(arr[l++], arr[r--]); // 自己不需要的数正是对方需要的
}
}
// k ——我们要取的第k小的数,此时必然有 l > r
// 但是最终由于最后一次的l++,r--; l指向第一个大于等于currNum的值,r指向第一个小于等于currNum的值,故为 sortNum(l, end)和 sortNum(begin, r)
if(k >= l) sortNum(l, end); // 表明在右区间[l, end]
else if(k <= r) sortNum(begin, r); //表明在左区间[begin, r]
//这种情况就是(r < k < l), 而且必定是中间提到的情况二:r-l=2, 如果是情况一:r-l=1, 那么[begin,r]和[l,end]已经覆盖了所有区域,不可能走这个分支!!!
//所以实际可能是 l = r, l++, r--, r- l = 2的情况, r + 1 = k = l - 1; 最后通过begin==end结束
else {
sortNum(r+1 , l-1);
}
}
int main(){
int n;
n = read();
k = read();
arr.resize(n);
for(int i = 0; i < n; i++){
arr[i] = read();
}
sortNum(0, n - 1);
cout << ans;
return 0;
}
3.注意
这里我一直有两个测试点过不去,经过资料查阅之后得知:cin,cout的读入输出效率过慢,因为他们有着缓冲机制和一些其他安全措施,但是这里读入的数1≤n<5000000过多
这里可以选择使用scanf和printf,或者像我这里使用快读的方法
这里的inline是内联函数的意思,直接将函数复制到相应位置,而不是调用函数,通过栈保存上下文信息,对于频繁使用的函数提高了效率。