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是内联函数的意思,直接将函数复制到相应位置,而不是调用函数,通过栈保存上下文信息,对于频繁使用的函数提高了效率。

posted @ 2024-02-13 22:33  DawnTraveler  阅读(81)  评论(0编辑  收藏  举报