题目描述:输入n个整数,输出其中最大的k个。
举例:输入序列1、2、3、4、5、6、7、8,输出最大的4个数字为5、6、7、8。
可能存在的条件限制:
要求 时间 和 空间消耗最小、海量数据、待排序的数据可能是浮点数等
方法一(不推荐):对所有元素进行排序,之后取出前K个元素
思路:使用最快排序算法,选择快排 或 堆排
时间复杂度:O(n*logn) + O(K) = O(n*logn)
特点:需要对全部元素进行排序,K = 1 时,时间复杂度也为O(n*logn)
注意:题中只需得到最大的K个数,而不需要对后面N-K个数排序
方法二(不推荐):只需要对前K个元素排序,不需要对N-K个元素进行排序
思路:使用 选择排序 或 起泡排序,进行K次选择,可得到第k大的数
时间复杂度:O(n*k)
解法一:O(n)的算法,需要修改数组值(需要交换数值),寻找最大的k个数
具体方法:使用类似快速排序,执行一次快速排序后,每次只选择一部分继续执行快速排序,直到找到第K个大元素为止,此时这个元素在数组位置后面的元素即所求。
在数组S中找出一个元素X,把数组分为两部分Sa和Sb。Sa中的元素大于等于X,Sb中元素小于X。
这时有两种情况:
1. Sa中元素的个数小于k,则Sb中的第k-|Sa|个元素即为第k大数;
2. Sa中元素的个数大于等于k,则返回Sa中的第k大数。
时间复杂度:因为每次只选择一部分递归,所以时间复杂度为N(1+1/2+1/4+....+1/x)<2N,所以复杂度为O(N).
若随机选取枢纽,线性期望时间O(N)。
若选取数组的“中位数”作为枢纽,最坏情况下的时间复杂度O(N)
以下代码用的是中位数法,得到最大的K个数。
下面的方法是找到最小的k个元素。如果想要找最大的k个元素,实际要找的是最小的size-k个元素。
枢纽元就选择第一个元素。
void Swap(vector<int>& input, int i, int j) {
int tmp = input[i];
input[i] = input[j];
input[j] = tmp;
}
int Partition(vector<int>& input, int start, int end) {
int privot = input[start];
int low = start + 1, high = end;
while (1) {
while (privot >= input[low]) ++low;
while (privot < input[high]) --high;
if (low < high) {
Swap(input, low, high);
++low;
--high;
} else {
break;
}
}
Swap(input, high, start);
return high;
}
void Recursion(vector<int>& input, int start, int end, int k) {
if (start >= end) {
return;
}
int mid = Partition(input, start, end);
if (k == mid+1) {
return;
} else if (k > mid+1) {
Recursion(input, mid + 1, end, k);//k在后半部分
} else {
Recursion(input, start, mid - 1, k);//k在前半部分
}
}
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
vector<int> ret;
int size = input.size();
if (k > size || k == 0) {
return ret;
}
Recursion(input, 0, size-1, k);
ret.assign(input.begin(), input.begin() + k);
return ret;
}
如果是求最小的k个数,可以有下面的解法二。
解法二:O(nlogk)算法,无需修改原有数组,特别适合处理海量数据
我们可以创建一个大小为k的容器来存储最小k的数字,接下来遍历数组,每次读入一个数。
如果容器中的数字个数少于k,则直接把这个数插入到容器中;
如果容器中已有k个数,此时我们不能再插入新的数字,而只是替换已有的数字。
当容器满了后,我们要做3件事:
1. 在已有的k个数中找到最大值;
2. 有可能在容器中删除最大值
3. 有可能要插入这个新的数。
如果我们用二叉树实现这个数据容器,那么我们可以在O(logk)时间内实现这三个步骤。
因此,对于n个数字而言,复杂度为O(nlogk)。
红黑树的查找、插入、删除时间复杂度都是O(logn),所以我们可以用红黑树实现这个容器。
stl中的set和multiset都是基于红黑树实现的,元素都是有序的。
因此我们可以用multiset(可以存放重复元素)实现这个容器。
具体代码如下:
type multiset<int, greater<int> > IntSet //比较方法是greater,所以set中的元素按从大到小排列
type multiset<int, greater<int> >::iterator IntSetIterator
IntSet GetLeastKNumbers(vector<int> &data, int k)
{
IntSet ret;
if(k < 1 || data.size() <= k)
{
return;//最小的k个数就是data
}
for(vector<int>::iterator it = data.begin(); it != data.end(); ++it)
{
if(ret.size() < k)
{
ret.insert(*it);
}
else
{
IntSetIterator it_greatest = ret.begin();//set中的最大值
if(*it_greatest > *it)//set中最大值大于这个数,则把最大值从set中删除,再插入这个数
{
ret.erase(it_greatest);
ret.insert(*it);
}
}
}
return ret;
}
两种解法的比较:
第一种解法,基于分区,即类似快速排序的方法,虽然时间复杂度是O(n),但是需要修改数组的值。
第二种解法虽然慢一点,但是有两个优点,一是无需修改原数组的值,二是适合海量数据处理。
我们每次读入data中的一个元素,读写都只是在大小为k的容器中进行的,如果是海量的数据,我们就不能把所有数据一次性装入内存,第一种方法就不行了。
而第二种方法判断是否需要把新数据放入到容器中,只要要求内存大小能够容纳这个容器即可,因此最适合的情形就是n很大,k很小的情况。
下表总结了这两种解法的特点: