输出最小的 k 个数

题目:输入 n 个数,找出其中最小的 k 个数,例如,输入 4, 5, 1, 6, 2, 7, 3, 8 这八个数字,则最小的 4 个数字为 1, 2, 3, 4.

该题最简单的思路莫过于把输入的 n 个整数排序,排序之后位于最前面的 k 个数就是最小的 k 个数,这种思路的时间复杂度是 O(nlogn)。但是,这里介绍更快的算法。

(一)O(n) 的算法,只有当可以修改输入数组时可用

从之前的题目“找出数组中出现次数超过一半的数字”可以得到启发,我们同样可以使用基于 Partition 函数来解决这个问题。如果基于数组的第 k 个数字来调整,似的比第 k 个数字小的所有数字都位于数组的左边,比第 k 个数字大的所有数字都位于数组的右边。这样调整之后,位于数组中左边的 k 个整数就是最小的 k 个数字(这 k 个数字不一定是排序的)。

//O(n)的算法,只有当可以修改输入的数组时可用
int RandomInRange(int start, int end)
{
 srand(time(NULL));
 return start + rand() % (end - start + 1);
}
void Swap(int *a, int *b)
{
 int temp = *a;
 *a = *b;
 *b = temp;
}
int Partition(int *numbers, int length, int start, int end)
{
 if(numbers == NULL || length <0 || start < 0 || end > length)
  return 0;
 int index = RandomInRange(start, end);
 Swap(&numbers[index], &numbers[end]);
 
 int leastNum = start - 1;
 for(index = start; index < end; index++)
 {
  if(numbers[index] < numbers[end])
  {
   leastNum++;
   if(leastNum != index)
    Swap(&numbers[leastNum], &numbers[index]);
  }
 }
 leastNum++;
 Swap(&numbers[leastNum], &numbers[end]);
 return leastNum;
}
void GetLeastNumbers(int *input, int n, int k)
{
 if(input == NULL || n <= 0 || k > n || k <= 0)
  return;
 int start = 0;
 int end = n - 1;
 int index = Partition(input, n, start, end);

 while(index != k - 1)
 {
  if(index > k - 1)
  {
   end = index - 1;
   index = Partition(input, n, start, end);
  }
  else
  {
   start = index + 1;
   index = Partition(input, n, start, end);
  }
 }
 for(int i = 0; i < k; i++)
  cout << input[i] << " ";
 cout << endl;
}

(二)O(nlogk) 的算法,特别适合处理海量数据

    先创建一个大小为 k 个数据容器来存储最小的 k 个数字,接下来每次从输入的 n 个整数中读入一个数。如果容器中已有的数字少于 k 个,则直接把这次读入的整数放入容器中;如果容器中已有 k 个整数,也就是容器已满,此时不能再插入新的数字,而只能替换已有的数字。找出这已有的 k 个数中的最大值,然后将这次带插入的整数和最大值比较。如果带插入的值比最大值小,则用这个数替换当前已有的最大值;如果带插入的数大于当前最大值,则不做任何操作。(当容器已满时需要做三件事:①在 k 个证书中找到最大的数;②有可能在这个容器中删除最大的数;③有可能要插入一个新的数字)。

    我们可以使用不同的二叉树来实现数据容器。由于每次都需要找到 k 个整数中的最大值,很容易想到最大堆。在最大堆中,根节点的值总是大于它的子树中的任意节点的值。于是可以再 O(1) 得到已有数据的最大值,但需要 O(logk) 时间完成删除及插入操作。

    然而从头实现一个最大堆需要一定的代码,我们可以使用红黑树来实现数据容器。红黑树通过把节点分为红,黑两种颜色并根据一些规则确保树在一定程度上式平衡的。从而保证在红黑树中的查找,删除, 插入操作都只需要 O(logk) 时间。在 STL 中 set 和 multiset 都是基于红黑树实现的。此处我们使用过的是 STL 中的 multiset。

//最小的 k 个数
#include<iostream>
#include<vector>
#include<set>
#include<time.h>
using namespace std;

//O(nlogk)的算法,特别适合处理海量数据
typedef multiset<int, greater<int>>  intSet;
typedef multiset<int, greater<int>>::iterator setIterator;

void GetLestNumbers(const vector<int>& data, intSet& leastNumbers, int k)
{
 leastNumbers.clear();
 if(k < 1 || data.size() < k)
  return;
 vector<int>::const_iterator iter = data.begin();
 for(; iter != data.end(); iter++)
 {
  if(leastNumbers.size() < k)
   leastNumbers.insert(*iter);
  else
  {
   setIterator iterGreater = leastNumbers.begin();
   if(*iterGreater > *iter)
   {
    leastNumbers.erase(iterGreater);
    leastNumbers.insert(*iter);
   }
  }
 }
 setIterator itera = leastNumbers.begin();
 for( ; itera != leastNumbers.end(); itera++)
  cout << *itera << " ";
 cout << endl;
}

int main()
{
vector<int> v;
 int value;
 while(cin >> value)
  v.push_back(value);
 int k = 5;
 intSet leastNumbers;
 GetLestNumbers(v, leastNumbers, k);
 system("pause");
 return 0;
}

两种解法的比较:

    基于 Partition 函数的第一种解法的平均时间复杂度是 O(n),比第二种思路要快,但同时它也有明显的限制,比如会修改输入数组。

    第二种解法虽然要慢一点,但是它有两个明显的优点:①没有修改输入的数据。② 该算法适合海量数据的输入(假设题目是要求从海量的数据中找出最小的 k 个数字,由于内存的大小是有限的,有可能不能把这些海量数据一次性的载入内存。此时,可以从辅助存储空间中每次读入一个数字,根据 GetLeastNumbers 的方式 判断是不是需要放入 LeastNumbers 即可。这种思路只要求内存能够容纳 leastNumbers 即可,因此,它最合适的情形就是 n 很大并且 k 较小的问题)。

你可能感兴趣的:(输入,N,个数,个数,K,找出其中最小的)