这道题目要求我们从一组学生分数中选出k个分数,使得这k个分数中最大值和最小值的差最小。乍一看可能觉得需要穷举所有可能的k个数的组合,但实际上通过排序可以大大简化问题。
经过仔细分析,我发现一个重要性质:最小差值一定出现在排序后的连续k个元素中。这是因为:
举个例子,假设排序后的数组是[1,4,7,9],k=2:
基于这个观察,我们可以设计出以下算法:
这个算法的时间复杂度主要由排序决定,为O(n log n),其中n是数组长度。滑动窗口的部分是O(n),所以整体复杂度是O(n log n)。
让我们仔细分析提供的C++代码:
cpp复制class Solution {
public:
int minimumDifference(vector<int>& nums, int k) {
// 第一步:对数组进行排序
sort(nums.begin(), nums.end());
// 初始化最小差值为最大可能值
int MinSub = INT_MAX;
// 滑动窗口遍历数组
for(int i=0; i<=nums.size()-k; i++){
// 计算当前窗口的差值
int tem = nums[i+k-1] - nums[i];
// 更新最小差值
if(tem < MinSub) MinSub = tem;
}
return MinSub;
}
};
排序操作:sort(nums.begin(), nums.end())使用C++标准库的快速排序算法,时间复杂度为O(n log n)。
滑动窗口:循环条件i<=nums.size()-k确保窗口不会越界。每次循环计算窗口最后一个元素nums[i+k-1]和第一个元素nums[i]的差。
差值更新:使用INT_MAX初始化最小差值,确保第一个计算出的差值一定会被记录。
代码已经考虑了以下边界情况:
虽然当前算法已经相当高效,但还可以考虑以下优化:
早期终止:如果在滑动窗口过程中发现差值为0,可以直接返回,因为这是可能的最小值。
部分排序:如果只需要找到k个元素,可以使用选择算法找到第k小的元素,然后在其附近寻找,但实际实现可能更复杂。
这个问题有几个有趣的变种:
最大最小差值:找k个元素使它们的最大最小差值最大。解法类似,但应该取排序后两端的元素。
加权差值:每个元素有权重,需要考虑权重的差值最小化。
多维数据:如果每个学生有多个分数,如何选择k个学生使各科分数差的和最小。
这是最优的,因为排序本身就需要O(n log n)时间。
如果使用原地排序算法,可以认为是O(1)空间。
定理:排序后连续k个元素的窗口中必包含最小差值。
证明:
假设存在一个不连续的k元素集合S,其差值比任何连续k元素集合都小。设S的最小元素为a,最大为b。由于数组已排序,a和b之间必然包含至少k-1个元素(因为它们本身不连续)。因此,存在一个包含a或b的连续k元素集合,其差值不大于b-a,矛盾。
这个算法可以应用于:
输入验证:
数值范围:
排序稳定性:
为了验证代码的正确性,应该设计以下几类测试用例:
基本测试用例:
边界测试用例:
性能测试用例:
特殊值测试:
python复制def minimumDifference(nums, k):
nums.sort()
min_diff = float('inf')
for i in range(len(nums) - k + 1):
current_diff = nums[i + k - 1] - nums[i]
min_diff = min(min_diff, current_diff)
return min_diff
java复制import java.util.Arrays;
class Solution {
public int minimumDifference(int[] nums, int k) {
Arrays.sort(nums);
int minDiff = Integer.MAX_VALUE;
for (int i = 0; i <= nums.length - k; i++) {
int currentDiff = nums[i + k - 1] - nums[i];
minDiff = Math.min(minDiff, currentDiff);
}
return minDiff;
}
}
javascript复制function minimumDifference(nums, k) {
nums.sort((a, b) => a - b);
let minDiff = Infinity;
for (let i = 0; i <= nums.length - k; i++) {
const currentDiff = nums[i + k - 1] - nums[i];
minDiff = Math.min(minDiff, currentDiff);
}
return minDiff;
}
忘记排序:直接使用滑动窗口而不排序,会导致无法找到真正的最小差值。
窗口边界错误:
初始化错误:
打印中间结果:在滑动窗口循环中打印当前窗口和计算的差值。
小测试用例:先用题目给的示例测试,再逐步增加复杂度。
边界测试:专门测试k=1和k=数组长度的情况。
随机测试:生成随机数组验证代码的正确性。
对于非常大的数组,可以考虑:
如果数据是流式输入的,无法一次性获取所有数据,如何修改算法?可能需要使用不同的数据结构如堆来维护当前窗口。
虽然快速排序在平均情况下很好,但在最坏情况下是O(n^2)。对于特定数据,可以考虑:
在实际编码比赛中,可以尝试以下优化:
输入输出优化:
减少函数调用:
使用更快的排序:
优化后的C++代码可能如下:
cpp复制class Solution {
public:
int minimumDifference(vector<int>& nums, int k) {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
sort(nums.begin(), nums.end());
const int n = nums.size();
int MinSub = INT_MAX;
const int end = n - k;
for(int i = 0; i <= end; ++i) {
const int diff = nums[i + k - 1] - nums[i];
if(diff < MinSub) {
MinSub = diff;
if(MinSub == 0) break; // 提前终止
}
}
return MinSub;
}
};
这个优化版本添加了输入输出加速,添加了提前终止条件,并使用const变量减少重复计算。对于大型输入,这些优化可以带来可观的性能提升。