题面
解法1(快排)
复杂度
时间复杂度:O(nlongn),取决于排序的快慢
空间复杂度:O(n)
思路
- 由于逻辑关系和常理,k<n成立,不用特判。
- 要求最小的k个数,最能直接想到的思路就是:
- 先将这些数排好序,前k个最小的数压入ans数组,最后返回就好了。
- 根据常用习惯,下面就用了sort快排和Vector的push_back解决了。
也可以排序后直接 return vector<int>({input.begin(), input.begin()+k});
代码
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
vector<int> ans; //开辟存放结果的数组
sort(input.begin(),input.end());
for(int i=0; i<k; i++){
ans.push_back(input[i]); //压入合法的数
}
return ans; //返回存放结果的数组
}
};
解法2(STL进阶:大根堆)
复杂度
时间复杂度:O(nlongk),堆的每次操作时间复杂度不超过O(logk)
空间复杂度:O(k)
思路
- 优先队列模拟大根堆来维护最小的k个数字
- 有很多排序算法,我们很容易想到通过选择复杂度更低的排序算法来优化我们的解法
- 这里就要用到大根堆维护最小的k个数字:
- 依次将n个数字放入堆中,若堆中数字超过k个,就说明此时堆中最大的数一定不属于前k个最小数,不断将堆顶元素出堆,就能得到想要的结果了。
大小根堆的介绍:
代码
class Solution {
public:
priority_queue<int> q;
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
// 将原数组的数放入优先队列
for(auto x:input){
q.push(x);
if(q.size()>k) q.pop();
}
input.clear();
//若达不到k个数字
if(q.size()<k) return input;
while(!q.empty()){
input.push_back(q.top());
q.pop();
}
return input;
}
};
手写大根堆代码
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if(input==null || input.length==0 || k>input.length || k==0)
return list;
int[] arr = new int[k+1];//数组下标0的位置作为哨兵,不存储数据
//初始化数组
for(int i=1; i<k+1; i++)
arr[i] = input[i-1];
buildMaxHeap(arr, k + 1);//构造大根堆
for(int i=k; i<input.length; i++) {
if(input[i] < arr[1]) {
arr[1] = input[i];
adjustDown(arr, 1, k+1);//将改变了根节点的二叉树继续调整为大根堆
}
}
for(int i=1; i<arr.length; i++) {
list.add(arr[i]);
}
return list;
}
public void buildMaxHeap(int[] arr, int length) {
if (arr == null || arr.length == 0 || arr.length == 1)
return;
for (int i = (length - 1) / 2; i > 0; i--) {
adjustDown(arr, i, arr.length);
}
}
// 堆排序中对一个子二叉树进行堆排序
public void adjustDown(int[] arr, int k, int length) {
arr[0] = arr[k];//哨兵
for (int i=2*k; i<=length; i*=2) {
if(i<length-1 && arr[i]<arr[i+1])
i++;//取k较大的子结点的下标
if(i>length-1 || arr[0]>=arr[i])
break;
else {
arr[k] = arr[i];
k = i; //向下筛选
}
}
arr[k] = arr[0];
}
};
解法3(快速选择算法)
复杂度
时间复杂度:期望O(n),n为数组长度,最坏时间复杂度为O(n^2)
空间复杂度:期望O(logn),递归调用的期望深度为logn,但最坏情况为O(n)
思路
- 根据题目可知,返回的数组不必是有序的,故我们可以想到快速选择算法来解决本问题,不熟悉快速选择算法的可以先看看第k个数这题,与其思路一致,我们可以利用快速排序的思想来实现
- 快速排序每次根据一个pivot点对区间进行划分,使得pivot左侧的数都比它小,右侧的数都比它大,然后再对两部分就行划分,而快速选择只需要选择其中一部分进行划分即可,根据k可以选择进入那个递归入口,这样的期望时间复杂度是线性的,但最坏情况下是O(n^2)的
- 算法步骤,首先选择数组中点为pivot,再使用双指针算法交换左侧大于等于pivot的数和右边小于等于pivot的数,最后我们可以保证数组被分为了以pivot划分的两部分,接着根据k选择递归入口即可,算法结束后我们可以保证数组的前k个数是最小的k个数,但是并不是升序的
代码
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
quickSelect(input, 0, input.size() - 1, k);
return vector<int>(input.begin(), input.begin() + k);
}
void quickSelect(vector<int>& input, int l, int r, int k){
if(l >= r) return;
int i = l - 1, j = r + 1, pivot = input[l + r >> 1];
// 令小于pivot的数都在左侧, 大于pivot的数都在右侧
while(i < j){
while(input[++i] < pivot);
while(input[--j] > pivot);
if(i < j) swap(input[i], input[j]);
}
int cnt = j - l + 1; // cnt表示小于等于pivot的数的数量
if(cnt >= k){ // 若cnt >= k, 说明答案在左区间
return quickSelect(input, l, j, k);
}
return quickSelect(input, j + 1, r, k - cnt); // 左边cnt个数已经确定, 只要确定右边的即可
}
};