算法第二章实践作业
算法第二章实践作业
一、问题描述
设计一个平均时间复杂度为 O(n) 的算法,在 n 个无序整数中找出第 k 小的数。
输入格式:第一行是 n 和 k,第二行是 n 个整数。
输出格式:第 k 小的数。
输入样例:
7 3
3 1 4 1 5 9 2
输出样例:
2
二、算法描述
核心思想
该算法称为快速选择(QuickSelect),其核心思想与快速排序相同,均采用划分(Partition)操作。快速排序在划分后需要递归处理基准两侧的两个子数组;而快速选择只需递归处理目标元素可能存在的那一侧,因为第 k 小的元素必定位于基准的某一侧或基准本身。
自然语言描述
第一步,取数组第一个数当基准,把数组重新排列,让基准左边都是小于等于它的,右边都是大于等于它的。排完之后,基准所在的位置就是它排好序之后的下标。
第二步,拿基准的位置和 k-1 比较(因为 k 是从 1 开始数的,数组下标从 0 开始):
- 如果相等,那基准就是第 k 小的数,直接返回
- 如果基准位置比 k-1 大,说明第 k 小在左边那段,去左边递归找
- 如果基准位置比 k-1 小,说明第 k 小在右边那段,去右边递归找
伪代码
partition(a, left, right):
x = a[left]
i = left, j = right
while i < j:
从右往左找第一个 < x 的数,放到 a[i]
从左往右找第一个 > x 的数,放到 a[j]
a[i] = x
return i
find(a, left, right, k):
if left >= right: 返回 a[left]
pos = partition(a, left, right)
if pos == k-1:
返回 a[pos]
else if pos > k-1:
返回 find(a, left, pos-1, k)
else:
返回 find(a, pos+1, right, k)
C++ 代码
#include <iostream>
using namespace std;
int a[10005];
int partition(int left, int right) {
int x = a[left];
int i = left, j = right;
while (i < j) {
while (i < j && a[j] >= x) j--;
a[i] = a[j];
while (i < j && a[i] <= x) i++;
a[j] = a[i];
}
a[i] = x;
return i;
}
int find(int left, int right, int k) {
if (left >= right) return a[left];
int pos = partition(left, right);
if (pos == k - 1) {
return a[pos];
} else if (pos > k - 1) {
return find(left, pos - 1, k);
} else {
return find(pos + 1, right, k);
}
}
int main() {
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
cout << find(0, n - 1, k) << endl;
return 0;
}
三、时间复杂度分析
最好情况 O(n)
每次划分都很均匀,而且刚好一次就找到目标了,只递归一次。递归式:
T(n) = n + T(n/2)
= n + n/2 + n/4 + ...
= 2n
= O(n)
最坏情况 O(n²)
每次选的基准都是当前区间里最大或者最小的数,划分极不均匀,每次只能排除一个元素,要递归 n 次。比如数组本来就是有序的,又每次都选第一个数当基准,就会出现这种情况。
T(n) = n + T(n-1)
= n + (n-1) + (n-2) + ... + 1
= n(n+1)/2
= O(n²)
平均情况 O(n)
随机数据下,每次划分大概均匀,而且只递归一边,总体还是 O(n)。如果担心最坏情况,可以随机选基准而不是固定选第一个,这样最坏情况基本不会出现。
四、对分治法的体会和思考
分治法的核心思想是将一个规模为 n 的问题分解为若干个规模较小的同类子问题,分别求解后再将子问题的解合并为原问题的解。分治的三个基本步骤为:分解(Divide)、求解(Conquer)、合并(Combine)。
通过快速选择和快速排序的对比可以看出,同样基于划分思想,快排需要递归处理左右两个子数组,时间复杂度为 O(n log n);而快速选择只递归目标元素可能存在的那一侧,时间复杂度为 O(n)。这说明在求解问题时,只需关注必要的部分,避免不必要的计算,可以显著提升效率。
分治法与动态规划的区别在于:分治法的子问题相互独立,不会重叠;动态规划的子问题存在重叠,需要通过记忆化或填表来避免重复计算。例如斐波那契数列,直接递归计算会产生大量重复调用,而动态规划自底向上填表则无此问题。
通过本章学习,我认识到递归不仅仅是函数调用自身,更重要的是如何将原问题合理地分解为规模更小的同类子问题。编写递归算法时,明确递归终止条件、正确划分子问题范围、合理合并子问题的解,是保证算法正确性和效率的关键。
浙公网安备 33010602011771号