❓ 剑指 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]
限制:
- 0 < = k < = a r r . l e n g t h < = 10000 0 <= k <= arr.length <= 10000 0<=k<=arr.length<=10000
- 0 < = a r r [ i ] < = 10000 0 <= arr[i] <= 10000 0<=arr[i]<=10000
💡思路:
法一:排序
对原数组从小到大排序后取出前 k
个数即可。
法二:堆
维护一个大小为 k
的最小堆过程如下:使用大顶堆
- 在添加一个元素之后,如果大顶堆 堆顶元素 小于
k
,那么将大顶堆的堆顶元素去除,也就是将当前堆中值最大的元素去除,从而使得留在堆中得元素都比被去除的元素来得小;
法三:快速选择
使用快速排序的
partions()
方法!
只有当允许修改数组元素时才可以使用!
快速排序的 partition()
方法,会返回一个整数 j
使得 a[l..j-1]
小于等于 a[j]
,且 a[j+1..h]
大于等于 a[j]
,此时 a[j]
就是数组的第 j
大元素。
可以利用这个特性找出数组的第 k
个元素,这种找第 k
个元素的算法称为 快速选择算法。
🍁代码:(C++、Java)
法一:排序
C++
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
vector<int> ans(k);
if(k > arr.size() || k == 0) return ans;
sort(arr.begin(), arr.end());
for (int i = 0; i < k; ++i) {
ans[i] = arr[i];
}
return ans;
}
};
Java
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] ans = new int[k];
if(k > arr.length || k == 0) return ans;
Arrays.sort(arr);
for (int i = 0; i < k; ++i) {
ans[i] = arr[i];
}
return ans;
}
}
法二:堆
C++
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
vector<int> ans(k);
if(k > arr.size() || k == 0) return ans;
priority_queue<int> maxHeap;
for(int num : arr){
maxHeap.push(num);
if(maxHeap.size() > k){
maxHeap.pop();
}
}
for(int i = 0; i < k; i++){
ans[i] = maxHeap.top();
maxHeap.pop();
}
return ans;
}
};
Java
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] ans = new int[k];
if(k > arr.length || k == 0) return ans;
//Java 的 PriorityQueue 实现了堆的能力,PriorityQueue 默认是小顶堆
//可以在在初始化时使用 Lambda 表达式 (o1, o2) -> o2 - o1 来实现大顶堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
for(int num : arr){
maxHeap.add(num);
if(maxHeap.size() > k){
maxHeap.poll();
}
}
for(int i = 0; i < k; i++){
ans[i] = maxHeap.poll();
}
return ans;
}
}
法三:快速选择
C++
class Solution {
private:
void findKthSmallest(vector<int>& arr, int k) {
int l = 0, h = arr.size() - 1;
while(l < h){
int j = partition(arr, l, h);
if(j == k) break;
else if(j > k) h = j - 1;
else l = j + 1;
}
}
int partition(vector<int>& arr, int l, int h) {
int p = arr[l]; // 选定标兵,划分元素
int i = l, j = h + 1;
while(true){
while(i < h && arr[++i] < p);//在左边找到一个比 p 大的元素
while(j > l && arr[--j] > p);//在右边找到一个比 p 小的元素
if(i >= j) break;
swap(arr[i], arr[j]);
}
swap(arr[l], arr[j]);
return j;
}
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
vector<int> ans(k);
if(k > arr.size() || k == 0) return ans;
/* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */
findKthSmallest(arr, k - 1);
for(int i = 0; i < k; i++){
ans[i] = arr[i];
}
return ans;
}
};
Java
class Solution {
private void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
private int partition(int[] arr, int l, int h){//划分
int p = arr[l];
int i = l, j = h + 1;
while(true){
while(i < h && arr[++i] < p);//在左边找到一个比 p 大的元素
while(j > l && arr[--j] > p);//在右边找到一个比 p 小的元素
if(i >= j) break;
swap(arr, i, j);
}
swap(arr, l, j);
return j;
}
private void findKthSmallest(int[] arr, int k) {
int l = 0, h = arr.length - 1;
while(l < h){//使得前 k 个数都是最小的 k 个数
int j = partition(arr, l, h);//划分
if(j == k) break;
else if(j > k) h = j - 1;
else l = j + 1;
}
}
public int[] getLeastNumbers(int[] arr, int k) {
int[] ans = new int[k];
if(k > arr.length || k == 0) return ans;
// findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数
findKthSmallest(arr, k - 1);
for(int i = 0; i < k; i++){
ans[i] = arr[i];
}
return ans;
}
}
🚀 运行结果:
🕔 复杂度分析:
法一:排序
-
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn),其中
n
为数组arr
的长度。算法的时间复杂度即排序的时间复杂度。 -
空间复杂度: O ( l o g n ) O(logn) O(logn),排序所需额外的空间复杂度为 O ( l o g n ) O(logn) O(logn)。
法二:堆
- 时间复杂度:
O
(
n
l
o
g
k
)
O(nlogk)
O(nlogk),其中
n
为数组arr
的长度。由于大根堆实时维护前k
小值,所以插入删除都是 O ( l o g k ) O(logk) O(logk) 的时间复杂度,数组里n
个数都会插入,所以一共需要 O ( n l o g k ) O(nlogk) O(nlogk) 的时间复杂度。 - 空间复杂度: O ( k ) O(k) O(k)。
法三:快速选择
- 时间复杂度:
O
(
n
)
O(n)
O(n),最坏情况下的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。情况最差时,每次的划分点都是最大值或最小值,一共需要划分
n−1
次,而一次划分需要线性的时间复杂度,所以最坏情况下时间复杂度为 O ( n 2 ) O(n^2) O(n2)。 - 空间复杂度: O ( l o g n ) O(logn) O(logn)。递归调用的期望深度为 O ( l o g n ) O(logn) O(logn),每层需要的空间为 O ( 1 ) O(1) O(1),只有常数个变量。最坏情况下的空间复杂度为 O ( n ) O(n) O(n)。
题目来源:力扣。
放弃一件事很容易,每天能坚持一件事一定很酷,一起每日一题吧!
关注我LeetCode主页 / CSDN—力扣专栏,每日更新!