40 最小的K个数(时间效率)
题目描述:
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
测试用例:
功能测试(输入的数组中有相同的数字;输入的数组中没有相同的数字)
边界值测试(输入的k等于1或者等于数组的长度)
特殊输入测试(k小于1;k大于数组的长度;指向数组的指针为NULL)
解题思路:
1)把数组排序后,前面的k个数就是最小的k个数。时间复杂度为O(nlogn) 面试官会提示,使用更快的方法。
2)当可以修改数组时,基于Partition函数的O(n)方法
基于Parition函数:基于数组中的第k个数字来调整,使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的都位于数组的右边。调整之后位于左边的k个数字就是最小的k个数字(这k个数字不一定是排序的)
class Solution { public: vector<int> GetLeastNumbers_Solution(vector<int> input, int k) { //特殊输入判断 vector<int> res; if(input.empty() || k<1 || k>input.size()) //不要忘记考虑k比数组长度大的情况!!! 对每个输入参数考虑非法输入 return res; int begin = 0; int end = input.size()-1; //基于Partition int index = Partition(input, begin, end); while(index!=k-1){ //第k个元素,索引为k-1 if(index>k-1){ end = index-1; index = Partition(input, begin, end); }else{ begin = index+1; index = Partition(input, begin, end); } } for(index=0;index<k;index++){ res.push_back(input[index]); } return res; } int Partition (vector<int> &input, int begin,int end){ if(end<begin ||begin<0 ||end>input.size()-1 || input.empty()) return -1; int index = RandomInRange(begin,end); swap(input[index],input[end]); //把选中的元素放到最后 int small = begin-1; for(index=begin;index<end;index++){ if(input[index]<input[end]){ small++; if(small!=index) swap(input[index],input[small]); } } small++; swap(input[end],input[small]); return small; } int RandomInRange(int begin,int end){ return (rand() %(end-begin+1) + begin); } };
代码注意:
-1. 最小的第k个元素,对应的索引是k-1
-2. 特殊输入的判断不要忘记考虑k比数组长度大的情况
k>input.size()
3)时间复杂度为O(nlogk)的算法,特别适合处理海量数据
思路:定义一个大小为k的数据容器来存储k个数,容器未满直接向容器中添加数字,容器已满,找出k个数字的最大值,然后与待插入的整数比较,如果待插入的值比当前已有的最大值小,则用这个数替换当前已有的最大值;如果待插入的值比当前最大值还要大,那么这个数不可能是最小的k个整数之一,抛弃这个整数。
容器满3个操作:1.找到最大值 2.删除最大值 3.插入最大值 用二叉树来实现这个容器,可以在O(logk)时间内实现这三个操作。因此对于N个输入的数字而言总的时间效率就是O(nlogk)
可以使用的数据结构:最大堆 或 红黑树
最大堆中,根节点的值总是大于它的子树中的任意节点的值。可以每次在O(1)时间内得到已有的k个数字中的最大值,但需要O(logk)时间完成删除以及插入操作。
红黑树通过把节点分为红、黑两种颜色并根据一些规则确保树在一定程度上是平衡的,从而保证在红黑树中的查找、删除和插入操作都只需要O(logk)时间。
在STL中的数据容器,set和multiset都是基于红黑树实现的。
实现1,使用升序数组
class Solution { public: vector<int> GetLeastNumbers_Solution(vector<int> input, int k) { //特殊输入判断 if(input.empty() || k<1 || k>input.size()) //不要忘记考虑k比数组长度大的情况!!! 对每个输入参数考虑非法输入 return vector<int>(); typedef multiset<int,greater<int>> intSet; //typedef multiset<int,greater<int>>::iterator setIterator; intSet leastNumbers; //比较好的方法是for循环时,定义常量迭代器 //vector<int>::const_iterator iter = input.begin(); for(auto iter = input.begin();iter!=input.end();iter++){ //遍历每一个元素 if(leastNumbers.size()<k) leastNumbers.insert(*iter); else{ if(*iter<*(leastNumbers.begin())) {//获取set的首元素,要解引用*(leastNumbers.begin()) //删掉首元素,并添加新的元素 leastNumbers.erase(leastNumbers.begin()); leastNumbers.insert(*iter); } } } return vector<int>(leastNumbers.begin(),leastNumbers.end()); } };
代码注意:
1. greater<int> 在库文件 #include<functional>中。表示内置类型从大到小排序。 less<int> 内置类型从小到大排序
2. 对迭代器解引用才是对应的值内容
实现2:使用降序数组
class Solution { public: vector<int> GetLeastNumbers_Solution(vector<int> input, int k) { //特殊输入判断 if(input.empty() || k<1 || k>input.size()) //不要忘记考虑k比数组长度大的情况!!! 对每个输入参数考虑非法输入 return vector<int>(); typedef multiset<int> intSet; typedef multiset<int,greater<int>>::iterator setIterator; intSet leastNumbers; //比较好的方法是for循环时,定义常量迭代器 //vector<int>::const_iterator iter = input.begin(); for(auto iter = input.begin();iter!=input.end();iter++){ //遍历每一个元素 if(leastNumbers.size()<k) leastNumbers.insert(*iter); else{ setIterator last = --leastNumbers.end(); if(*iter<*(last)) {//获取set的首元素,要解引用*(leastNumbers.begin()) //删掉首元素,并添加新的元素 leastNumbers.erase(last); leastNumbers.insert(*iter); } } } return vector<int>(leastNumbers.begin(),leastNumbers.end()); } };
注意:
对迭代器iter
*(iter++) 对iter解引用,然后将iter向后移动一位
(*iter)++ 对iter解引用,然后对解引用后的内容++
*(++iter) 将iter向后移动一位,然后对当前位置(移动后的迭代器位置)解引用
*iter++ 对iter解引用,然后将iter向后移动一位
4)最大堆 时间复杂度O(nlogk)(时间复杂度不确定)
class Solution { public: vector<int> GetLeastNumbers_Solution(vector<int> input, int k) { int len=input.size(); if(len<=0 || k>len || k<=0) return vector<int>(); //注意是k<=0;没有等号的话,程序不通过 vector<int> res(input.begin(),input.begin()+k); //建堆 make_heap(res.begin(),res.end()); for(int i=k;i<len;i++) { if(input[i]<res[0]) { //先pop,然后在容器中删除 pop_heap(res.begin(),res.end()); res.pop_back(); //先在容器中加入,再push res.push_back(input[i]); push_heap(res.begin(),res.end()); } } //使其从小到大输出 sort_heap(res.begin(),res.end()); return res; } };
代码注意:
- make_heap()生成堆,他有两个参数,也可以有三个参数,前两个参数是指向开始元素的迭代器和指向结束元素的下一个元素的迭代器。第三个参数是可选的,可以用伪函数less()和greater()来生成大顶堆和小顶堆,其中type为元素类型。如果只传入前两个参数,默认是生成大顶堆。 [first,last) 这个区间是半开半闭的。
- push_heap()是在堆的基础上进行数据的插入操作,参数与make_heap()相同,需要注意的是,只有make_heap()和push_heap()同为大顶堆或小顶堆,才能插入。执行push_heap 时, [first,last-1)个元素是保持堆形态的,如果不是堆,则会报错。
- pop_heap()是在堆的基础上,弹出堆顶元素。这里需要注意的是,pop_heap()并没有删除元素,而是将堆顶元素和数组最后一个元素进行了替换,如果要删除这个元素,还需要对数组进行pop_back()操作。 使用pop_heap 操作后, 最大值被移动到last-1的位置。[first ,last-1) 之间的元素继续保持堆的形态。