【剑指offer】:求最小的K个数

版权声明:本文为博主原创文章,欢迎转载,转载请声明出处! https://blog.csdn.net/hansionz/article/details/82891660

题目描述: 输入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

猜你喜欢

转载自blog.csdn.net/hansionz/article/details/82891660
今日推荐