题目描述: 输入N个数,找出其中最小的K个数。例如,输入1,2,3,4,5,6,7,8
,求最小的4个数,既输出1,2,3,4
。
解题思路一: 这道题我们最直接的想法就是将这些数按照升序排序
,然后取前K个数
,就是我们最终想要的到的结果,现在较好一点的排序方法时间复杂度是NlogN
,我们还有更快的实现方法吗?
代码实现:
对于各种排序的总结见我的另一篇博客:
https://blog.csdn.net/hansionz/article/details/82828547
解决思路二: 根据一次快排(Partition)的想法,我们知道一次随机快速排序可以确定一个有序的位置
,这个位置的左边都小于这个数
,右边都大于这个数
,我们如果能找到随机快速排序确定的位置等于k-1的那个位置
,那么0-k-1
个数就是我们要找的数。怎么能找到那个位置:
- 如果Partition确定的位置
小于K-1
,说明k-1这个位置在它的右边
,我们继续在右边进行查找。 - 如果Partition确定的位置
大于K-1
,说明k-1这个位置在它的左边
,我们继续在左边进行查找。
代码实现:
//交换两个数
void Swap(int* x1, int* x2)
{
int tmp = *x1;
*x1 = *x2;
*x2 = tmp;
}
//一次快排(左右指针法)
int Partition(int* input, int start, int end)
{
assert(input);
int begin = start;
int last = end;
int key = input[end];
while (begin<last)
{
while ((begin < last) && (input[begin] <= key))
begin++;
while ((begin<last) && (input[last]>=key))
last--;
Swap(&input[begin], &input[last]);
}
Swap(&input[begin], &input[end]);
return begin;
}
//求数组中最小的K个数
//1.根据一次快排方法,找到K在一次快排中的位置,左边的就是最小的K个数
//时间复杂度O(n)
void MinKNumber(int* input, int* output, int len, int k)
{
assert(input&&output);
if (len <= 0 || k <= 0 || k > len)
return;
int start = 0;
int end = len - 1;
//第一次快速排序
int index = Partition(input, start, end);
//根据二分思想找以k-1为基准的一次快排
while (index != k - 1)
{
if (index > k - 1)
{
end = index - 1;
index = Partition(input, start, end);
}
else
{
start = index + 1;
index = Partition(input, start, end);
}
}
//把最小的K个数放到output数组中输出
for (int i = 0; i < k; i++)
{
output[i] = input[i];
}
}
缺点: 这种方法的时间复杂度虽然是O(n)
,但是找出来的最小的K个数却不是排序过的
。而且这种方法有个限制,就是必须修改给的数组
。
对于Partition函数还存在几种方法,见我的另一篇博客:
https://blog.csdn.net/hansionz/article/details/82821811
解决思路三: 本方法使用于海量数据处理。
大致思想是建一个K个数的大堆
,每次拿一个数和堆顶元素比较
,如果这个数比堆顶元素大
,则必然不是最小的K个数
,如果这个数比堆顶元素小
,则与堆顶元素交换
,然后在向下调整一次
建成新的大堆,然后遍历所有的数,直到最小的K个数都进堆
里。
- 最大的K个数----
建小堆
- 最小的K个数----
建大堆
代码实现:
//2.topK问题,建一个最大堆
void AdjustDown(int* input, int k, int parent)
{
if (input == NULL || k <= 0)
return;
int child = 2 * parent + 1;
while (child < k)
{
if ((child + 1 < k) && (input[child] < input[child + 1]))
++child;
if (input[child]>input[parent])
{
Swap(&input[child], &input[parent]);
parent = child;
child = 2 * parent + 1;
}
else
break;
}
}
//每次拿一个数和堆顶元素比较,如果比它小,则入堆,比它大则不入,继续向后查找
//适用于大数据问题,时间复杂度O(nlogn)
void MinKNumber_op(int* input, int len, int k)
{
assert(input);
int i = 0;
//1.利用前K个数建大堆
for (i = (2 * k - 2) / 2; i >= 0; i--)
{
AdjustDown(input, k, i);
}
//2.遍历后边的数,如果小于堆顶元素则入堆
for (i = k; i < len; i++)
{
if (input[i] < input[0])
{
Swap(&input[i], &input[0]);
AdjustDown(input, k, 0);
}
}
}
这种方法的时间复杂度是O(NlogN)
,但是非常适于于大数据求解,因为当数据很大时,利用前两种方法是不能求解的,内存放不下,只能利用磁盘,读入一个数,和堆顶元素比较重新建堆。
堆的相关操作见我的另一篇博客:
https://blog.csdn.net/hansionz/article/details/81984631