1.题目
输入n
个整数,找出其中最小的K
个数。例如输入4,5,1,6,2,7,3,8
这8
个数字,则最小的4
个数字是1,2,3,4
。
2.我的题解
2.1最小堆
使用最小堆实现:
- 实现一个最小堆,借助
vector
,下标从1
开始存储; - 实现删除最小元素的函数
del
,要求删除后保持堆有序,返回删除的元素; - 利用输入数据建堆;
- 连续执行
K
次del
,并将函数返回值添加到输出结果。 - 时间复杂度:
O(n+klogn)
- 空间复杂度:
O(n)
下沉操作构建堆:
- 确定堆的大小,开辟空间并填入数值;
- 从第一个非叶节点开始做下沉操作,直到第一个节点。
构建堆的复杂度计算: 以n
个节点,树高h
的满二叉树(堆)为例,显然h=log(n+1)
;
各层节点关系: 设想一个满二叉树,下一层的节点数总是翻倍,而总结点个数约等于最后一层节点个数的两倍(实际上就少1);故每一层的节点数和都可以由n
表示。
堆的层数 | 下沉时比较次数 | 最大节点数 | 最大总操作数 |
---|---|---|---|
1 | h | 1/2h * (n+1) | h/2h * (n+1) |
2 | h-1 | 1/2h-1 * (n+1) | (h-1)/2h-1 * (n+1) |
3 | h-2 | 1/2h-2 * (n+1) | (h-2)/2h-2 * (n+1) |
…… | …… | …… | …… |
k | h-k | 1/2h+1-k * (n+1) | (h-k)/2h+1-k * (n+1) |
…… | …… | …… | …… |
h | 1 | 1/2 * (n+1) | 1/2 * (n+1) |
S = ∑ k = 1 h k 2 k = 1 2 + 2 4 + ⋯ + h 2 h S 2 = ∑ k = 1 h k 2 k + 1 = 1 4 + 2 8 + ⋯ + h 2 h + 1 错 位 相 减 得 : S 2 = 1 2 + 1 4 + 1 8 + ⋯ + 1 2 h − h 2 h + 1 = ( 1 − 1 2 h + 1 ) − h 2 h + 1 < 1 T = h 2 h ( n + 1 ) + h − 1 2 h − 1 ( n + 1 ) + ⋯ + 1 2 ( n + 1 ) = ( n + 1 ) ∑ k = 1 h k 2 k < ( n + 1 ) ∗ 2 \begin{aligned} S=&\sum_{k=1}^{h}\frac{k}{2^k}=\frac{1}{2}+\frac{2}{4}+\cdots+\frac{h}{2^h}\\ \frac{S}{2}=&\sum_{k=1}^{h}\frac{k}{2^{k+1}}=\frac{1}{4}+\frac{2}{8}+\cdots+\frac{h}{2^{h+1}}\\ 错位相减&得:\\ \frac{S}{2}=&\frac{1}{2}+\frac{1}{4}+\frac{1}{8}+\cdots+\frac{1}{2^{h}}-\frac{h}{2^{h+1}}\\ =&(1-\frac{1}{2^{h+1}})-\frac{h}{2^{h+1}}\\ <&1\\ T=&\frac{h}{2^h}(n+1)+\frac{h-1}{2^{h-1}}(n+1)+\cdots+\frac{1}{2}(n+1) \\ =&(n+1)\sum_{k=1}^{h}\frac{k}{2^k} \\ <&(n+1)*2 \end{aligned} S=2S=错位相减2S==<T==<k=1∑h2kk=21+42+⋯+2hhk=1∑h2k+1k=41+82+⋯+2h+1h得:21+41+81+⋯+2h1−2h+1h(1−2h+11)−2h+1h12hh(n+1)+2h−1h−1(n+1)+⋯+21(n+1)(n+1)k=1∑h2kk(n+1)∗2
因此T=O(n)
。
class Solution {
vector<int> vec;//小顶堆
int N = 0;
void swap(int * a, int * b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void sink(int k) {
while ((k<<1) <= N) {
int j = k << 1;
if (j+1<=N && vec[j] > vec[j + 1])j++;
if (vec[k] > vec[j])swap(&vec[k], &vec[j]);
k = j;
}
}
int del() {
swap(&vec[1], &vec[N--]);
sink(1);
return vec[N+1];
}
void makeHeap(vector<int> &data) {
N = data.size();
vec.resize(N << 1);
for(int i=0;i<data.size();i++)vec[i+1]=data[i];
for(int i=N/2;i>=1;i--)
sink(i);
}
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
vector<int> res;
if (k == 0 || input.empty() || k>input.size())return res;
//make heap
makeHeap(input);
//find
while (k-- > 0) {
int tmp = del();
//cout << tmp << " ";
res.push_back(tmp);
}
return res;
}
};
3.别人的题解
3.1 大顶堆
构造大小为K
的大顶堆而不是小顶堆:
- 遍历输入数据;
- 如果堆的规模小于等于
K
:添加元素到大顶堆中; - 如果堆的规模大于
K
:如果当前数大于堆顶元素,跳过;如果当前元素小于堆顶,则用该元素代替堆顶元素,并进行操作保证堆有序。 - 时间复杂度:
O(nlogk)
- 空间复杂度:
O(k)
;该方法适合海量数据。
class Solution {
vector<int> vec;//大顶堆
int N = 0;
void swap(int * a, int * b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void sink(int k) {
while ((k << 1) <= N) {
int j = k << 1;
if (j + 1 <= N && vec[j] < vec[j + 1])j++;
if (vec[k] < vec[j])swap(&vec[k], &vec[j]);
k = j;
}
}
void makeHeap(vector<int> &data, int K) {
vec.resize(K + 1);
N = K;
int i = 0;
//1~K
for (i = 0; i<K; i++) {
vec[i + 1] = data[i];
}
for (i = K / 2; i >= 1; i--)sink(i);
//K~n
for (i = K; i<data.size(); i++) {
if (data[i]<vec[1]) {
vec[1] = data[i];
sink(1);
}
}
}
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
vector<int> res;
if (k == 0 || input.empty() || k>input.size())return res;
//make heap
makeHeap(input, k);
//find
res.assign(vec.begin() + 1, vec.begin() + k + 1);
return res;
}
};
3.2 partition思想
利用快排中的partition
思想对[low,high]
进行操作,如果进行partition
操作返回的值:
- 初始化:
low=0,high=len(input)-1
; - 小于
K
:low++
; - 大于
K
:high--
; - 等于
K
:返回输入数组的前K
个元素; - 该方法改变输入数据,不需要额外空间;每次
partition
时间复杂度为O(n)
(n
为输入数据的规模),最好情况下只需要执行1
次,最坏情况下需要执行n
次; - 时间复杂度:
O(n)~O(n^2)
- 空间复杂度:
O(1)
class Solution {
void swap(int * a, int * b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int partition(vector<int> &s,int left,int right){
int pivot=s[left];
int i=left;
for(int j=left+1;j<=right;j++){
if(s[j]<=pivot){
i++;
if(i!=j)swap(&s[i],&s[j]);
}
}
swap(&s[left],&s[i]);
return i;
}
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
vector<int> res;
if (k == 0 || input.empty() || k>input.size())return res;
//partition
int low=0,high=input.size()-1;
int index=partition(input,low,high);
while(index!=k-1){
if(index>k-1)high--;
else low++;
index=partition(input,low,high);
}
res.assign(input.begin(),input.begin()+k);
return res;
}
};
4.总结与反思
(1)Markdown
数学公式。
(2)partition
思想。
(3)大顶堆、小顶堆及建堆、用途。
4.1 大顶堆小顶堆的比较
建堆需要O(n)
的复杂度,具体近似是2n
,下沉操作复杂度是O(logn)
,故建堆加找最小的K
个数:
- 大顶堆:时间复杂度
O(2n+klogn)
,空间复杂度O(n)
; - 小顶堆:时间复杂度
O(2k+nlogk)
,空间复杂度O(k)
;
闲来无聊,我分析了一波本题中大顶堆实现和小顶堆实现的时间耗费,我直觉地觉着,大顶堆已经耗费更多的空间了,时间上应该会好一些吧……n,k
都是可变的,三维曲面图画了没能清晰地看出变化趋势,于是固定了n
,观察不同k
下的时间耗费:
- 如图可以看到,基本上
k>8
时,大顶堆的时间耗费将更低,k=n
时,二者时间耗费一样。 - 横坐标是
k
,纵坐标是时间耗费,计算公式如上。
4.2 partition的实现
pivot的选择:
- 选取第一个元素;
- 选取最后一个元素;
- 选取第一个、中间、最后一个元素的中位数:该方法性能一般较好。
- 随机选取数组中某个值:性能较好,但
random()
并不便宜。
4.2.1 单向扫描法实现partition
int partition1(vector<int> s, int left, int right) {
int pivot = s[left];
int i = left ;
for (int j = left+1; j <= right; j++) {
if (s[j] <= pivot) {
i++;
if (i != j)swap(&s[i], &s[j]);
}
}
swap(&s[left], &s[i]);
return i;
}
4.2.2 双向扫描法实现partition(哨兵法)
void swap(int *a, int *b) {
int tmp = *a; *a = *b; *b = tmp;
}
int partition2(vector<int> s, int left, int right) {
int pivot = s[left];
int i = left, j = right;
while (i < j) {
while (i<j&&s[j]>pivot)j--;
while (i < j&&s[i] <= pivot)i++;
if (i < j)swap(&s[i], &s[j]);
}
swap(&s[left], &s[i]);
return i;
}
4.2.3 双向扫描法实现partition(挖坑法)
int partition3(vector<int> s, int left, int right) {
int pivot = s[left];
int i = left, j = right;
while (i < j) {
while (i<j && s[j]>pivot)j--;
if (i < j)s[i++] = s[j];
while (i < j&&s[i] <= pivot)i++;
if (i < j)s[j--] = s[i];
}
s[i] = pivot;
return i;
}
4.2.4 partition三路切分
假设数组的值仅有0,1,2
三种取值,需要将全部0
放到最左边,全部2
放最右边,全部1
放中间,可通过以下步骤实现:
- 获取数组长度
len
,初始化i=0,low=0,high=len-1
; - 从两端向中间扫描,同时从左向右扫描;
- 可以理解为
low
的左边全是0
,i
指向当前数值,high
的右边全是2
;只要把全部的0
移到左边,全部的2
移到右边,那么剩下的1
就自然全在中间了; - i位置上是
1
,i
右移; - 位置上是
0
,交换i
和low
位置,同时都右移; - i位置上是
2
,交换i
和high
位置,high
左移,但i
不右移,因为交换后i
位置可能是0
,就还需要一次交换; - 完成后
low
指向第一个1
,high
指向第一个2
,如需要确切的划分位置可利用。
#include <iostream>
#include <stack>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
#include <queue>
#include <stdio.h>
#include <list>
#include <set>
using namespace std;
void partitionThreeWay(vector<int> &s, int left, int right) {
int i = left , low = left, high = right;
while (i <= high) {
if (s[i] == 1)i++;
else if (s[i] == 0)swap(&s[i++], &s[low++]);
else swap(&s[i], &s[high--]);
}
//输出内容
for (int t = left; t <= right; t++) {
if (t == low)cout << low << " ";
else if (t == i)cout << i << " ";
else cout << "_ ";
}
cout << endl;
for (int t = left; t <= right; t++) {
cout << s[t] << " ";
}
cout << endl;
}
int main()
{
vector<int> s = { 0,1,2,2,1,0,1,2,0 };
partitionThreeWay(s,0, s.size() - 1);
system("pause");
return 0;
}