题目:输入 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 较小的问题)。