剑指 Offer 40. 最小的k个数
剑指 Offer 40. 最小的k个数
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
方法一:排序
思路和算法
对原数组从小到大排序后取出前 k 个数即可。
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
vector<int> vec(k, 0);
sort(arr.begin(), arr.end());
for (int i = 0; i < k; ++i) vec[i] = arr[i];
return vec;
}
};
时间复杂度:O(nlogn),其中 n 是数组 arr 的长度。算法的时间复杂度即排序的时间复杂度。
空间复杂度:O(logn),排序所需额外的空间复杂度为 O(logn)。
方法二:优先队列
#include<iostream>
#include <queue>
#include<vector>
using namespace std;
int main(){
priority_queue<int,vector<int> ,greater<int> >q;
int num,n;
cin>>num;
q.push(num);
while (cin.get()!='\n')
{
cin>>num;
q.push(num);
}
cin>>n;
for (int i = 0; i < n; i++)
{
cout<<q.top()<<" ";
q.pop();
}
}
思路和算法
我们用一个大根堆实时维护数组的前 k小值。首先将前 k 个数插入大根堆中,随后从第 k+1个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。在下面的代码中,由于 C++ 语言中的堆(即优先队列)为大根堆,我们可以这么做。
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
vector<int>vec(k, 0);
if (k == 0) return vec; // 排除 0 的情况
priority_queue<int>Q;
for (int i = 0; i < k; ++i) Q.push(arr[i]);
for (int i = k; i < (int)arr.size(); ++i) {
if (Q.top() > arr[i]) {
Q.pop();
Q.push(arr[i]);
}
}
for (int i = 0; i < k; ++i) {
vec[i] = Q.top();
Q.pop();
}
return vec;
}
};
复杂度分析
时间复杂度:O(nlogk),其中 n 是数组 arr 的长度。由于大根堆实时维护前 k小值,所以插入删除都是 O(logk) 的时间复杂度,最坏情况下数组里 n 个数都会插入,所以一共需要 O(nlogk) 的时间复杂度。
空间复杂度:O(k),因为大根堆里最多 k 个数。
方法三:快排思想
思路和算法
我们可以借鉴快速排序的思想。我们知道快排的划分函数每次执行完后都能将数组分成两个部分,小于等于分界值 pivot 的元素的都会被放到数组的左边,大于的都会被放到数组的右边,然后返回分界值的下标。与快速排序不同的是,快速排序会根据分界值的下标递归处理划分的两侧,而这里我们只处理划分的一边。
我们定义函数 randomized_selected(arr, l, r, k) 表示划分数组 arr 的 [l,r] 部分,使前 k 小的数在数组的左侧,在函数里我们调用快排的划分函数,假设划分函数返回的下标是 pos(表示分界值 pivot 最终在数组中的位置),即 pivot 是数组中第 pos - l + 1 小的数,那么一共会有三种情况:
如果 pos - l + 1 == k,表示 pivot 就是第 kk 小的数,直接返回即可;
如果 pos - l + 1 < k,表示第 kk 小的数在 pivot 的右侧,因此递归调用 randomized_selected(arr, pos + 1, r, k - (pos - l + 1));
如果 pos - l + 1 > k,表示第 kk 小的数在 pivot 的左侧,递归调用 randomized_selected(arr, l, pos - 1, k)。
函数递归入口为 randomized_selected(arr, 0, arr.length - 1, k)。在函数返回后,将前 k 个数放入答案数组返回即可。
class Solution {
int partition(vector<int>& nums, int l, int r) {
int pivot = nums[r];
int i = l - 1;
for (int j = l; j <= r - 1; ++j) {
if (nums[j] <= pivot) {
i = i + 1;
swap(nums[i], nums[j]);
}
}
swap(nums[i + 1], nums[r]);
return i + 1;
}
// 基于随机的划分
int randomized_partition(vector<int>& nums, int l, int r) {
int i = rand() % (r - l + 1) + l;
swap(nums[r], nums[i]);
return partition(nums, l, r);
}
void randomized_selected(vector<int>& arr, int l, int r, int k) {
if (l >= r) return;
int pos = randomized_partition(arr, l, r);
int num = pos - l + 1;
if (k == num) return;
else if (k < num) randomized_selected(arr, l, pos - 1, k);
else randomized_selected(arr, pos + 1, r, k - num);
}
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
srand((unsigned)time(NULL));
randomized_selected(arr, 0, (int)arr.size() - 1, k);
vector<int>vec;
for (int i = 0; i < k; ++i) vec.push_back(arr[i]);
return vec;
}
};
复杂度分析
时间复杂度:期望为 O(n) ,由于证明过程很繁琐,所以不再这里展开讲。具体证明可以参考《算法导论》第 9 章第 2 小节。最坏情况下的时间复杂度为 O(n^2)
。情况最差时,每次的划分点都是最大值或最小值,一共需要划分 n - 1次,而一次划分需要线性的时间复杂度,所以最坏情况下时间复杂度为 O(n^2)。
空间复杂度:期望为 O(logn),递归调用的期望深度为 O(logn),每层需要的空间为 O(1),只有常数个变量。
最坏情况下的空间复杂度为 O(n)。最坏情况下需要划分 n 次,即 randomized_selected 函数递归调用最深 n - 1层,而每层由于需要 O(1) 的空间,所以一共需要 O(n) 的空间复杂度。
因上求缘,果上努力~~~~ 作者:图神经网络,转载请注明原文链接:https://www.cnblogs.com/BlairGrowing/p/13534444.html