434
社区成员




找第k小的数的分治算法,通常称为“快速选择”(Quickselect)算法,它是快速排序(Quicksort)算法的一个变种。快速选择算法的基本思想是,通过一次划分(partition)操作,将待排序的序列分成两部分,其中一部分的所有元素都比另一部分的所有元素要小。然后,根据划分的结果和k的值,递归地在包含第k小元素的那一部分继续划分,直到找到第k小的元素。
以下是自然语言描述的快速选择算法:
输入:一个无序数组arr
和一个整数k
,表示要找到第k
小的元素(注意,这里的k
通常是从1开始计数的)。
选择一个基准元素(pivot):从数组中选择一个元素作为基准。选择基准元素的方法有多种,例如随机选择、选择第一个元素、选择最后一个元素、选择中间元素等。为了简化描述,这里假设选择第一个元素作为基准。
划分操作:以基准元素为基准,将数组划分为两部分。一部分包含所有小于基准的元素,另一部分包含所有大于或等于基准的元素。划分操作结束后,基准元素在其最终排序后的数组中的位置也就确定了,记为pos
。
判断k的位置:
k == pos + 1
(注意要加1,因为数组索引是从0开始的,而k是从1开始计数的),那么基准元素就是第k小的元素,算法结束。k < pos + 1
,说明第k小的元素在基准元素的左侧部分,递归地在左侧部分继续快速选择。k > pos + 1
,说明第k小的元素在基准元素的右侧部分,递归地在右侧部分继续快速选择,但此时要注意更新k的值,因为右侧部分的元素是从一个新的起始位置开始计数的。具体来说,新的k值应该是k - (pos + 1)
(因为已经跳过了基准元素及其左侧的所有元素)。输出:找到第k小的元素并返回。
#include <iostream>
using namespace std;
void swap(int &a,int &b)
{
int temp=a;
a=b;
b=temp;
}
int partition(int *a, int left, int right) {
int i = left;
int j = right;
int temp = a[left];
while (i <j) {
while (i<j&& a[j] > temp) j--;
if (i<j) swap(a[i++], a[j]);
while (i < j && a[i] < temp) i++;
if (i < j) swap(a[i], a[j--]);
}
return i;
}
int find(int a[], int left, int right, int k) {
int mid= partition(a, left, right);
if (k == mid+1 ) return a[mid];
else if (k < mid+1) return find(a, left, mid - 1, k);
else return find(a, mid + 1, right, k );
}
int main() {
int N, k;
cin >> N >> k;
int a[1000];
for (int i = 0; i < N; i++) {
cin >> a[i];
}
int num = find(a, 0, N - 1, k);
cout << num << endl;
return 0;
}
在最好的情况下,快速选择算法每次选择的基准元素都能将数组均匀地划分为两部分。这意味着,每次划分后,搜索范围都会减半,从而形成一个深度为 log N
的递归树(其中 N
是数组的大小)。因此,在最好的情况下,快速选择算法的时间复杂度是 O(N log N)
的一个子集,即 O(N)
,因为每次划分操作本身需要 O(N)
的时间(遍历整个数组以重新排列元素)。然而,由于我们只需要找到第 k
小的元素,而不需要对整个数组进行排序,所以实际的最好时间复杂度是 O(N)
。
但需要注意的是,这里的 O(N)
是指除了递归调用栈外的额外操作(如元素比较和交换)的时间复杂度。递归调用栈的深度在最好的情况下是 O(log N)
,但由于我们关注的是整体时间复杂度,而递归调用栈的空间复杂度通常不被计入时间复杂度分析中。
在最坏的情况下,快速选择算法每次选择的基准元素都是当前划分中的最大或最小值(这通常是由于选择了数组的第一个、最后一个或中间元素作为基准,并且数组已经接近有序)。这会导致每次划分后,搜索范围只减少了一个元素(即基准元素被放到了正确的位置上,但其他元素的位置几乎没有变化)。因此,在最坏的情况下,快速选择算法会退化成线性搜索,时间复杂度变为 O(N^2)
,因为每次划分都需要 O(N)
的时间,并且需要进行 N
次划分(在最坏的情况下)。
然而,通过一些改进策略(如随机选择基准元素、使用“三数取中”法来选择基准等),可以大大降低遇到最坏情况的可能性,从而使快速选择算法在实际应用中通常表现良好。
分治法是一种非常重要的算法设计范式,它在计算机科学和数学领域都有广泛的应用。以下是我对分治法的体会和思考:
简化问题:分治法通过将复杂问题分解成更小、更简单的子问题来求解,这使得原本难以处理的问题变得易于管理。每个子问题都可以独立解决,然后合并子问题的解以得到原问题的解。
递归与迭代:分治法通常使用递归来实现,但也可以通过迭代来实现。递归实现简洁直观,但需要注意递归深度可能导致的栈溢出问题。迭代实现则更加稳健,但需要额外维护一些状态信息。
并行与分布式:由于分治法将问题分解成多个独立的子问题,这些子问题可以并行处理,从而加速求解过程。在分布式计算环境中,分治法特别有用,因为它可以自然地映射到多个计算节点上。
性能优化:虽然分治法的最坏时间复杂度可能较高(如快速排序在最坏情况下的时间复杂度为O(N^2)),但通过选择合适的划分策略(如随机选择基准),可以大大降低最坏情况发生的概率,使算法在实际应用中表现良好。
选择适当的基准:在分治法中,基准的选择对算法的性能有重要影响。一个好的基准可以平衡子问题的大小,使递归树更加均衡,从而减少递归深度和时间复杂度。
合并操作的优化:在解决子问题后,需要合并子问题的解以得到原问题的解。合并操作的效率直接影响整个算法的性能。因此,在设计分治算法时,需要仔细考虑如何高效地合并子问题的解。
分治法的局限性:虽然分治法非常强大,但它并不适用于所有问题。对于某些问题,分治法可能无法有效地将问题分解成独立的子问题,或者合并操作的代价太高。在这种情况下,需要考虑其他算法设计范式。
与其他算法的结合:分治法可以与其他算法设计范式结合使用,以形成更强大的算法。例如,分治法可以与动态规划结合使用来解决某些优化问题;可以与哈希表结合使用来加速查找操作;还可以与并行计算技术结合使用来加速大规模数据处理。
总之,分治法是一种非常强大且灵活的算法设计范式,它通过将复杂问题分解成更小、更简单的子问题来求解,使得原本难以处理的问题变得易于管理。在实际应用中,需要根据问题的特点和需求来选择合适的划分策略和合并方法,以实现高效的算法。