找出第k大的数[No. 64]

本文介绍了一种在未排序数组中查找第K大元素的有效算法——SELECT算法,该算法通过将数组划分为多个子数组并利用中位数的中位数进行分区,能够在O(N)的时间复杂度内找到目标值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题:

从一个数组里面,找出第K大的数。

题目很简单,要想把第K个数找出来,其实也挺容易的。

第一种方法:无非就是先排序,比如用Merge Sort算法,整个算法复杂度为 O(NlgN), 然后找到第K个即可。

第二种方法:如果k很小,比如第五个最大的数,而整个数组的长度非常的大,那么,还有一种方法就是,我做k遍找最大的数,每做一遍,就把最大的放在数组的最后面,然后减少数组扫描的范围,就可以把第k大的数找出来,这样做的复杂度就是O(K*N),在K很小的情况下,还是不错的。

第三种方法:我们可以借助quicksort的思想,把数组的值分成两部分,一部分比那个pivot大,一部分比pivot小,因为我们知道pivot在数组中的位置,所以比较k和pivot的位置就知道第k大的值在哪个范围,我们不断的进行recursion, 直到pivot就是第K大的值。时间复杂度,出乎意料,为O(N),但是这是平均复杂度。 为何它的平均复杂度比quicksort的复杂度低呢?重要原因是quicksort要对pivot两边的子数组还要排序,而我们其实只需要对其中一个进行处理,所以复杂度更低。具体怎么推导,请参考算法导论。

但是本文讲的是另一个算法,叫做SELECT 算法,它能在时间复杂度为O(N)的情况下找出第K大的数。先把算法贴出来,然后再讲。


第一步:把数组分成\lfloor n/5 \rfool 这么多子数组,每个子数组里包含5个数,因为会有无法整出的可能,所以最后一个子数组会小于5.

第二步:用insertion sorting 把这5个数排序,然后找出中位数,也就是第3个。

第三步:把获得的中位数又排序,找出中位数的中位数。如果中位数的个数是偶数,那么取排好序的第 m/2 个数,m指的是中位数的个数。

第四步:然后呢,把原来的数组分成两个部分,一部分比那个“中位数的中位数”大,一部分比那个“中位数的中位数”小。我们可以假设左边的数大,右边的数小。然后我们可以得到“中位数的中位数”的位置i.

第五步:如果i = k, 那么那个“中位数的中位数”就是第k大的数。如果 i < k, 不用说,第k大的在“中位数的中位数”的右边,否则就在左边。我们一直recursely 这么做,那么就一定能够找到第K大的值了。

其实,算法还是比较容易懂得,关键的关键,是复杂度的分析。如果能够知道复杂度如何求出来的,那么,对算法本身就了解得更清楚。


要讲复杂度,首先看一个图。


图中的X 就是“中位数的中位数”, 而且箭头的方向是从大数指到小数。所以,我们可以知道,至少灰色区域的都比X大,这是整个复杂度分析的关键,而,其它点能否说它比X大,我们不能保证。而灰色区域里最多有多少个数呢?因为X是中位数的中位数,所以,比X大的中位数最少有 [(\lfloor n/5 \rfool) * (1/2) - 2] 个(这个值也是关键), 这里减2是因为要去除X本身,第二呢,还要去除一个中位数---这个中位数所在的子数组个数小于5.  所以,最坏最坏的情况,第K大的值不在灰色区域里,那么我们就要对剩下部分进行不断的SELECT。剩余部分就是n - 3 [(\lfloor n/5 \rfool) * (1/2) - 2] = O(7n/10) .

整个过程中,第1,2,4步所需时间为O(n), 注意第2步的复杂度不为O(n^2),第3步的复杂度为 T(n/5),第五步的复杂度为 T(7n/10)。

所以,复杂度的递归公式为: T(n) =  T(n/5) + T(7n/10) + O(n), 算出来以后T(n) = O(n).





### C语言回溯法实现子集和等于k的算法 以下是基于回溯法求解子集和问题的C语言实现代码。此方法按照深度优先策略搜索解空间树,并通过剪枝函减少不必要的计算。 ```c #include <stdio.h> #define MAX_SIZE 100 int n, c; // 集合小n,目标和c int X[MAX_SIZE]; // 原始集合组 int Y[MAX_SIZE]; // 当前子集组 int count = 0; // 打印当前找到的子集 void printSubset() { printf("{ "); for (int i = 0; i < n; i++) { if (Y[i]) { // 如果第i个元素被选入子集中 printf("%d ", X[i]); } } printf("}\n"); } // 回溯法核心逻辑 void subsetSum(int index, int currentSum) { if (currentSum == c) { // 若当前和等于目标值,则打印结果 printSubset(); return; } if (index >= n || currentSum > c) { // 超过范围或超出目标值则返回 return; } // 不选择当前元素的情况 Y[index] = 0; subsetSum(index + 1, currentSum); // 选择当前元素的情况 Y[index] = 1; subsetSum(index + 1, currentSum + X[index]); Y[index] = 0; // 恢复状态以便后续分支使用 } int main() { scanf("%d %d", &n, &c); // 输入集合小和目标和 for (int i = 0; i < n; i++) { scanf("%d", &X[i]); // 输入集合中的元素 } subsetSum(0, 0); // 开始回溯搜索 if (!count) { // 如果没有任何解输出"No Solution!" printf("No Solution!\n"); } return 0; } ``` #### 解析说明 上述程序实现了基于回溯法的子集和问题求解过程。具体如下: - **输入部分**:读取集合小`n`、目标和`c`以及原始集合`X`的内容[^2]。 - **递归调用**:通过`subsetSum`函递归地探索每一种可能的选择情况(包含某个元素与否),并记录符合条件的结果[^4]。 - **剪枝条件**:如果当前累加和已经超过目标值`c`,或者已经遍历到集合末尾,则停止进一步深入搜索[^3]。 - **输出处理**:一旦发现满足条件的子集就立即输出;如果没有找到任何有效子集,则最终提示“No Solution!”。 #### 时间复杂度分析 由于需要穷举所有潜在可能性,最坏情况下时间复杂度达到\( O(2^n) \)。对于较的输入规模而言效率较低,但在小型实例上表现尚可接受。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值