C++ 未排序数组中第 k 个最小/最大元素 | 最坏情况下的线性时间

目录

例如

方法

实现上述想法的步骤

示例代码

详细时间复杂度分析

递推关系式变为

代入递推式

结论


如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。

未排序数组中第 k 个最小/最大元素 | 最坏情况下的线性时间( K'th Smallest/Largest Element in Unsorted Array | Worst case Linear Time**)**

给定一个包含不同整数的数组arr\[\]和一个整数k。任务是找出数组中第 k 小的元素。为了便于理解, k指的是如果将数组按升序排列,则位于第 k 个位置的元素。注意: k 始终小于数组的大小。

例如

输入: arr\[\] = 7, 10, 4, 3, 20, 15, k = 3

输出: 7

**说明:**排序后的数组为 3, 4, 7, 10, 15, 20。第三小的元素是 7。

输入: arr\[\] = 12, 3, 5, 7, 19, k = 2

输出: 5

**说明:**排序后的数组为 3, 5, 7, 12, 19。第二小的元素是 5。

输入: arr\[\] = 1, 5, 2, 8, 3, k = 4

输出: 5

在之前的"未排序数组中第 k 个最小/最大元素 预期线性时间"文章中,我们探讨了一种预期时间复杂度为线性的算法。本文将讨论一种最坏情况下时间复杂度为线性的方法。

未排序数组中第 k 个最小/最大元素 预期线性时间:

C++ 未排序数组中第 k 个最小/最大元素 预期线性时间 https://blog.csdn.net/hefeng_aspnet/article/details/163051233

C# 未排序数组中第 k 个最小/最大元素 预期线性时间 https://blog.csdn.net/hefeng_aspnet/article/details/163051552

Java 未排序数组中第 k 个最小/最大元素 预期线性时间 https://blog.csdn.net/hefeng_aspnet/article/details/163051446

Python 未排序数组中第 k 个最小/最大元素 预期线性时间 https://blog.csdn.net/hefeng_aspnet/article/details/163051495

JavaScript 未排序数组中第 k 个最小/最大元素 预期线性时间 https://blog.csdn.net/hefeng_aspnet/article/details/163051600

方法

这段代码的思路与QuickSelect()函数的基本思想相同,都是通过围绕一个枢轴点对数组进行分区来找到第 k 小的元素。但与 QuickSelect() 不同的是,QuickSelect() 可能会选择不好的枢轴点,导致最坏情况下的时间复杂度降至O(n²),而该算法通过使用"中位数的中位数"技术精心选择枢轴点,确保了最坏情况下的线性时间复杂度。我们希望枢轴点能够保证分区的合理平衡,并非完全平衡,但也并非极度偏斜。这意味着枢轴点应该确保数组的大部分都位于其两侧。这就是"中位数的中位数"策略的作用所在。

实现上述想法的步骤

1、为了找到合适的枢轴点,我们将数组分成每组5 个元素。这个大小(5)是一个关键的观察结果,它既足够小,可以实现快速排序,又足够大,可以确保在划分过程中达到数学上可证明的平衡。

2、每个组独立排序,并将其中位数收集到一个名为"中位数"的新列表中。

3、收集完所有中位数后,我们递归地找到这个中位数列表的中位数。这个值就成为我们的枢轴值。这样做的目的是为了避免使用错误的枢轴值,使其尽可能接近整个数组的真实中位数。

4、确定枢轴点后,我们使用标准逻辑对数组进行分区(左移元素小于等于枢轴点,右移元素大于枢轴点)。函数 partitionAroundPivot() 将枢轴点移动到正确的位置并返回该位置。

5、现在,我们将这个枢轴点的位置与所需的第 k 个索引进行比较。如果匹配,则直接返回该值作为答案。

6、否则,我们决定是在枢轴的左侧还是右侧进行递归:

6.1、如果枢轴位于第 k 个位置之后,则答案位于左侧子数组中。

6.2、如果枢轴位于第 k 个位置之前,则相应地调整 k,并对右侧子数组递归。

示例代码

// C++ implementation of the Worst Case Linear Time algorithm

// to find the k-th smallest element using Median of Medians

#include <bits/stdc++.h>

using namespace std;

// Returns median of a small group (size <= 5)

int getMedian(vector<int> &group) {

sort(group.begin(), group.end());

return groupgroup.size() / 2;

}

// Function to Partition array from index

// l to r around the pivot value x

int partitionAroundPivot(vector<int> &arr,

int l, int r, int x) {

// Move pivot x to end

int i;

for (i = l; i < r; i++) {

if (arri == x) break;

}

swap(arri, arrr);

// Standard partition logic

i = l;

for (int j = l; j < r; j++) {

if (arrj <= x) {

swap(arri, arrj);

i++;

}

}

swap(arri, arrr);

// Final position of pivot

return i;

}

// Recursively finds the k-th smallest element in arrl..r

int selectKthSmallest(vector<int> &arr, int l, int r, int k) {

if (k > 0 && k <= r - l + 1) {

int n = r - l + 1;

vector<int> medians;

int i;

// Divide array into groups of 5 and store their medians

for (i = 0; i < n / 5; i++) {

vector<int> group(arr.begin() + l + i * 5,

arr.begin() + l + i * 5 + 5);

medians.push_back(getMedian(group));

}

// Handle the last group with less than 5 elements

if (i * 5 < n) {

vector<int> lastGroup(arr.begin() + l + i * 5,

arr.begin() + l + i * 5 + (n % 5));

medians.push_back(getMedian(lastGroup));

}

// Find median of medians

int pivot;

if (medians.size() == 1) {

pivot = medians0;

} else {

pivot = selectKthSmallest(medians, 0, medians.size() - 1,

medians.size() / 2);

}

// Partition array and get position of pivot

int pos = partitionAroundPivot(arr, l, r, pivot);

// If position matches k, return result

if (pos - l == k - 1) return arrpos;

// Recur on left or right part accordingly

if (pos - l > k - 1)

return selectKthSmallest(arr, l, pos - 1, k);

return selectKthSmallest(arr, pos + 1, r, k - pos + l - 1);

}

return INT_MAX;

}

// Function to find kth Smallest in Array

int kthSmallest(vector<int> &arr, int k) {

return selectKthSmallest(arr, 0, arr.size() - 1, k);

}

// Driver code

int main() {

vector<int> arr = {7, 10, 4, 3, 20, 15};

int k = 3;

cout << kthSmallest(arr, k);

return 0;

}

输出

7

**时间复杂度:**O(n),最坏情况下选择时间为线性时间;

**空间复杂度:**O(n),在每次选择调用中递归存储中位数需要额外的空间。

详细时间复杂度分析

我们逐步分析中位数算法的最坏情况时间复杂度:

1、将数组分成 5 个元素一组的组。共有 n/5 组这样的组。由于每组元素的大小是固定的,因此求每组的中位数需要O(1) 的时间。所以,这一步的总时间复杂度为O(n)。

2、求中位数的中位数。递归地求 n/5 个中位数的中位数需要T(n/5)时间。

3、使用标准分区操作围绕枢轴(中位数的中位数)对数组进行分区需要O(n)时间。

4、划分完成后,递归调用会根据第 k 个最小元素所在的位置,向一侧(左侧或右侧)进行。

为了解递归调用的规模,我们分析有多少元素保证大于或小于枢轴值。n/5 个中位数中至少有一半大于或等于中位数的中位数。每一组中位数至少贡献 3 个大于中位数的中位数的元素。因此,大于或等于枢轴值的元素个数至少为(3 * ceil(1/2 * ceil(n/5)) - 6),即至少为(3n/10 - 6)。类似地,小于枢轴值的元素个数至少为 3n/10 - 6。

因此,在最坏的情况下,递归调用最多会处理 n - (3n/10 - 6) = 7n/10 + 6 个元素。

递推关系式变为

当 n <= 80 时,T(n) <= Θ(1);当

n > 80 时,T(n) <= T(ceil(n/5)) + T(7n/10 + 6) + O(n)。

我们通过代换法证明 T(n) = O(n)。假设对于某个常数 c,对于所有 n > 80,都有 T(n) <= cn。

代入递推式

T(n) <= c(n/5) + c(7n/10 + 6) + O(n)

= cn/5 + 7cn/10 + 6c + O(n)

= 9cn/10 + 6c + O(n)

我们可以选择足够大的 c,使得 cn/10 >= 6c + O(n),因此 T(n) <= cn。

因此,最坏情况下的运行时间是线性的,O(n)。

结论

尽管该算法在最坏情况下是线性的,但由于递归开销和多次迭代,涉及的常数项很大。实际上,随机化的快速选择算法速度更快,尽管其性能仅针对平均情况,但通常仍是首选。

如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。

相关推荐
phltxy10 小时前
C语言操作符详解
java·c语言·算法
RuoZoe11 小时前
从 2026 年 3 月 1 日开源,到 26.10.9:Jalium UI 半年时间到底走了多远?
c语言·c++
aqiu11111111 小时前
【LeetCode 902】最大为 N 的数字组合 - 详细题解与数位组合思路
数据结构·算法·leetcode·蓝桥杯·数位dp
辰烨chenye11 小时前
LeetCode Hot 100 题解 · 哈希篇
算法·leetcode·哈希算法
罗西的思考12 小时前
DreamZero 与 DreamDojo:世界模型与策略的分层协同综合分析与对比
人工智能·算法·机器学习
鱼子星_12 小时前
【C++】继承和多态(上)
c++·笔记
玖玥拾12 小时前
LeetCode 219 存在重复元素 II
算法·leetcode·哈希算法·散列表
知无不研13 小时前
c语言中循环的介绍与简单应用
c语言·开发语言·算法·循环·for·while