题目:输入n个数,找出其中最小的K个数。例如输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1,2,3,4。
思路:这道题目是典型的top K问题。两种方法:
(1)如果允许改变数列,半快速排序,是基准值正好为第K个数,那么基准值左边的都是小于它的,即可得到最小的K个数(求最大的过程类似),时间复杂度O(n)。
(2)不允许改变数列,采用维护数组的方法,建立一个最大堆。遍历数列,如果数字小于堆顶,那么堆顶很明显不是最小的K个数,更新堆顶,直到所有数字都已经与堆顶比较,堆上的数字即为最小的K个数。
半快速排序在面试题29里讲过,在此不再讨论。见连接:http://blog.csdn.net/moses1213/article/details/51072029
堆的实现可以用数组,也可以利用STL里的multiset容器,下面是分别用两种实现过程的求解代码:
1.用multiset实现
typedef multiset<int, greater<int> > intSet;
typedef multiset<int, greater<int> >::iterator setIterator;
void GetLeastNumbers(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 iterGreatest = leastNumbers.begin();
if(*iter < *(leastNumbers.begin())
{
leastNumbers.erase(iterGreatest);
leastNumbers.insert(*iter);
}
}
}
}
2.用数组实现
如果数组从0开始计数(也有从1开始),顺序存储堆的结点,那么对于结点i,它的左右子结点和父亲有这样的关系:
左儿子:2*i+1
右儿子:2*i+2
父亲: (i-1)/2
建堆过程:上滤建堆。因为数组总是在末尾插入新的元素,这时候比较它和父亲的大小,比父亲小则交换,逐步上移,直到大于父结点。
插入过程:和建堆过程类似,上滤法。
维护二叉堆的过程是每次和堆顶元素比较,此时需要采取的是下滤过程,因此这里需要上滤和下滤两个例程。
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
void SiftUp(int* a, int n)
{
int c;
for(int i = n - 1; i > 0 && a[i] > a[c=(i-1)/2]; i = c)
Swap(a[i], a[c]);
}
void SiftDown(int* a, int n)
{
int c;
for(int i = 0; (c = 2*i+1) <= n-1; i = c)
{
if(c+1 <= n-1 && a[c+1] > a[c])
++c;
if(a[i] >= a[c])
break;
Swap(a[i], a[c]);
}
}
TopK算法:
void TopK(int *a, int n, int k)
{
int *heap = new int[k];
for(int i = 0; i < k; ++i)
heap[i] = a[i]; //堆初始化数据
for(int i = 1; i < k; ++i)
SiftUp(heap, i+1); //建堆
for(int i = k; i < n; ++i) //与堆顶元素比较,如果小于堆顶则替换堆顶
{
if(a[i] < *heap)
{
*heap = a[i];
SiftDown(heap, k);
}
}
for(int i = 0; i < k; ++i)
cout << heap[i] << " ";
cout << endl;
delete[] heap;
}