题目:输入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; }