剑指 Offer 40. 最小的k个数
思路#
方法一:排序#
对原数组从小到大排序后取出前 k 个数即可。
时间复杂度:O(nlogn),其中 n 是数组 arr 的长度。算法的时间复杂度即排序的时间复杂度。
方法二:堆#
我们用一个大根堆实时维护数组的前 kk 小值。首先将前 kk 个数插入大根堆中,随后从第 k+1k+1 个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。
时间复杂度:O(nlogk),其中 n 是数组 arr 的长度。由于大根堆实时维护前 k 小值,所以插入删除都是 O(logk) 的时间复杂度,最坏情况下数组里 n 个数都会插入,所以一共需要O(nlogk) 的时间复杂度。
空间复杂度:O(k),因为大根堆里最多 k 个数。
1 class Solution { 2 public: 3 vector<int> getLeastNumbers(vector<int>& arr, int k) { 4 vector<int> res; 5 if(k <= 0) 6 return res; 7 8 priority_queue<int, vector<int>, less<int>> Q; 9 10 //先把前k个数放入大顶堆 11 for(int i = 0; i < k; ++i) 12 Q.push(arr[i]); 13 14 for(int i = k; i < arr.size(); ++i) { 15 if(arr[i] < Q.top()) { 16 Q.pop(); 17 Q.push(arr[i]); 18 } 19 } 20 21 while(!Q.empty()) { 22 res.push_back(Q.top()); 23 Q.pop(); 24 } 25 26 return res; 27 } 28 };
方法三:利用快排的思想进行划分#
这里可以参考:《算法导论(第3版)》的9.2和9.3节。
我们可以借鉴快速排序的思想。我们知道快排的划分函数每次执行完后都能将数组分成两个部分,小于等于分界值 pivot 的元素的都会被放到数组的左边,大于pivot的都会被放到数组的右边。
与快速排序不同的是,快速排序会根据分界值的下标递归处理划分的两侧,而这里我们只处理划分的一边,并返回分界值pivot的下标i。但是划分代码和快速排序中的代码是一模一样的。
所以左边的数就是最小的k个数,则将这k个数装入答案数组;
时间复杂度:期望为 O(n),具体证明可以参考《算法导论 (第3版)》第 9.2节。
对于最好的情况:每次所选的pivot划分之后正好在数组的正中间,那么递归方程为T(n) = T(n/2) + n,解得T(n) = O(n),所以此时此算法是O(n)线性复杂度的。
对于最坏的情况:每次的划分点都是最大值或最小值,即每次所选的pivot划分之后都好在数组最边上,一共需要划分 n - 1次,而每次划分需要O(n)的时间复杂度,所以此时此算法时间复杂度为O(n2)。
可以改进:改进选取主元的方法,使每次选出的主元在划分之后都能接近数组的中间位置,这样每次划分都能减少当前区间一半元素的工作量,可以使最坏情况下的时间复杂度降为O(n)。关于这种改进后的主元选取方法,见《算法导论(第3版)》的9.3节和这篇BFPRT算法的文章。
空间复杂度:期望为 O(logn),递归调用的期望深度为 O(logn),每层需要的空间为 O(1),只有常数个变量。最坏情况下的空间复杂度为 O(n)。最坏情况下需要划分 n次,而每层由于需要 O(1)的空间,所以一共需要 O(n) 的空间复杂度。
1 class Solution { 2 public: 3 vector<int> getLeastNumbers(vector<int>& arr, int k) { 4 if(k <= 0 || arr.empty()) return vector<int>(); 5 return partition(arr, k, 0, arr.size()-1); 6 } 7 8 /*和快速排序的划分代码一模一样*/ 9 vector<int> partition(vector<int>& arr, int k, int left, int right) { 10 int pivot = arr[left]; 11 12 int i = left, j = right; 13 while(i < j) { 14 while(j > i && arr[j] >= pivot) --j; 15 while(i < j && arr[i] <= pivot) ++i; 16 17 swap(arr[i], arr[j]); 18 } 19 20 swap(arr[left], arr[i]); 21 22 //先判断,有选择的对左半边或者右半边进行递归 23 if(i == k-1) { 24 vector<int> res; 25 for(int t = 0; t <= i; ++t) { 26 res.push_back(arr[t]); 27 } 28 return res; 29 } else if(i > k-1) { 30 return partition(arr, k, left, i-1); 31 } else { 32 return partition(arr, k, i+1, right); 33 } 34 } 35 };
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南