【LeetCode & 剑指offer 刷题笔记】目录(持续更新中...)
40 最小的k个数
题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,
/*
//暴力法:sort, O(nlogn)
//方法一:使用自带的stl函数
#include <algorithm>
using namespace std;
class Solution
{
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k)
{
if(k<=0 || k > input.size()) return vector<int>(); //处理非法输入
nth_element(input.begin(), input.begin()+k-1, input.end());
vector<int> result(input.begin(),input.begin()+k); //构造结果向量
return result;
}
};*/
/*
/*掌握
方法一:基于partition函数(快排中有用到,stl中也有,但是还是自己实现较好)
多次partition直到枢轴位置为k即可
缺点:会改变输入数组的元素位置
平均O(n),每次partition 平均O(n),次数未知,真的是O(n)吗,存疑?
思考:最好情况为O(n),最坏情况为O(n^2)(如对于倒序排列的数组)
通过优化partition,比如三数中值枢轴法或随机初始化枢轴法,可以改善时间复杂度
分析参考:
考虑最坏情况下,每次 partition 将数组分为长度为 N-1 和 1 的两部分,然后在长的一边继续寻找第 K 大,此时时间复杂度为 O(N^2 )。
不过如果在开始之前将数组进行随机打乱,那么可以尽量避免最坏情况的出现。
而在最好情况下,每次将数组均分为长度相同的两半,运行时间 T(N) = N + T(N/2),时间复杂度是 O(N)。
*/
#include <cstdlib>
class Solution
{
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k)
{
if(input.empty() || k<=0 || k > input.size()) return vector<int>(); //处理异常输入
int left = 0, right = input.size()-1;
int pivot_pos;
while(left <= right)//类似二分查找法
{
pivot_pos = partition(input, left, right);//如果要求最大的第k个数,可以对partition函数进行改造
if(pivot_pos < k-1)
left = pivot_pos + 1;
else if(pivot_pos > k-1)
right = pivot_pos - 1;
else
break;//此题要求的是返回最小的前k个数,如果仅返回最小的第k个数,直接在这里return a[pivot_pos]即可
}
vector<int> result(input.begin(), input.begin()+k); //构造结果向量
return result;
}
private:
int partition(vector<int>& a, int left, int right)
{
//随机初始化枢轴 5ms
//srand(time(nullptr)); //以当前时间为随机生成器的种子
//int pivotpos = rand()%(right - left + 1) + left; //产生【left,right】之间的数
//swap(a[pivotpos], a[left]); //将枢轴暂时放入起始位置
int pivot = left; //枢轴位置 4ms
while(left<right)
{
while(left < right && a[right] >= a[pivot]) right--; //找到本次扫描中第一个不满足枢轴规律的高位数
while(left < right && a[left] <= a[pivot]) left++; //找到本次扫描中第一个不满足枢轴规律的低位数
swap(a[left], a[right]); //交换以使满足枢轴规律
}//最后结果是left和right均指向枢轴位置
swap(a[left], a[pivot]); //将枢轴移动到位
return left; //返回枢轴位置
}
};
//
/*掌握
方法二:使用堆或者红黑树(平衡二叉搜索树)
用容器存储k个数,遍历输入向量过程中不断更新容器内的数(如果当前数小于容器中的最大值,则插入该数,删除原最大数)
优点:不需要修改输入数组,且适用于处理海量输入数据
O(nlogk)
*/
class Solution
{
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k)
{
if(input.empty() || k<=0 || k > input.size()) return vector<int>(); //处理异常输入
//仿函数中的greater<T>模板,从大到小排序(默认从小到大,左结点<父结点<根结点)
multiset<int,greater<int>> leastNums; //用红黑树存储这k个数
for(int ai:input)
{
if(leastNums.size() < k) leastNums.insert(ai); //将前k个元素插入容器
else
{
//第一个数为最大数
multiset<int, greater<int>>::iterator greatest_it = leastNums.begin();
//如果后续元素小于第一个元素,删除第一个,加入当前元素
if(ai < *greatest_it)
{
leastNums.erase(greatest_it);//删除原最大值
leastNums.insert(ai); //插入新元素(logk复杂度)
}
}
}
return vector<int>(leastNums.begin(), leastNums.end()); //返回结果向量(前k个最小的数)
}
};
Kth Largest Element in an Array
Find the kth largest element in an unsorted array. Note that it is the kth largest element in the sorted order, not the kth distinct element.
Example 1:
Input: [3,2,1,5,6,4] and k = 2
Output: 5
Example 2:
Input: [3,2,3,1,2,4,5,5,6] and k = 4
Output: 4
Note:
You may assume k is always valid, 1 ≤ k ≤ array's length.
暴力法:直接sort,O(nlogn),leetcode用时12ms
/*掌握
方法一:基于partition函数(快排中有用到,stl中也有,但是还是自己实现较好)
多次partition直到枢轴位置为k即可
缺点:会改变输入数组的元素位置
leetcode 耗时4ms,若用pivot = left的做法,则耗时20ms
平均O(n),O(1)
每次partition 平均O(n),次数未知,真的是O(n)吗,存疑?
思考:最好情况为O(n),最坏情况为O(n^2)(如对于倒序排列的数组)
通过优化partition,比如三数中值枢轴法或随机初始化枢轴法,可以改善时间复杂度
*/
#include <cstdlib>
class Solution
{
public:
int findKthLargest(vector<int>& a, int k)
{
if(a.empty() || k<=0 || k > a.size()) return 0; //处理异常输入
int left = 0, right = a.size()-1;
int pivot_pos;
while(left <= right)//类似二分查找法
{
pivot_pos = partition(a, left, right);//如果要求最大的第k个数,可以对partition函数进行改造
if(pivot_pos < k-1)
left = pivot_pos + 1;
else if(pivot_pos > k-1)
right = pivot_pos - 1;
else
return a[pivot_pos];
}
}
private:
int partition(vector<int>& a, int left, int right)
{
srand(time(nullptr)); //以当前时间为随机生成器的种子
int pivotpos = rand()%(right - left + 1) + left; //产生【left,right】之间的数
swap(a[pivotpos], a[left]); //将枢轴暂时放入起始位置
int pivot = left; //枢轴位置
while(left<right)
{
//改造为从大到小partition,注意符号的变化
while(left < right && a[right] <= a[pivot]) right--; //找到本次扫描中第一个不满足枢轴规律的高位数
while(left < right && a[left] >= a[pivot]) left++; //找到本次扫描中第一个不满足枢轴规律的低位数
swap(a[left], a[right]); //交换以使满足枢轴规律
}//最后结果是left和right均指向枢轴位置
swap(a[left], a[pivot]); //将枢轴移动到位
return left; //返回枢轴位置
}
};
方法二:维护一个堆或者平衡二叉查找树存储这k个数
/*掌握
改进:维护一个大小为k的小顶堆,扫描输入数据,不断更新小顶堆的内容
最后堆顶元素即可n个数中第k大的数
leetcode耗时4ms
每次堆调整平均时间复杂度为O(logk),共n次调整,故时间复杂度为O(nlogk)
O(nlogk), O(k)
*/
#include <algorithm>
#inlude <queue>
class Solution
{
public:
int findKthLargest(vector<int>& a, int k)
{
if(a.empty() || k<=0 || k>a.size()) return 0;//处理非法输入(可依题目返回适当值)
priority_queue<int,vector<int>,greater<int>> minheap; //构建小顶堆
for(int i = 0; i < a.size();i++)
{
if(i <= k-1)
minheap.push(a[i]); //将前k个元素插入容器
else
{
if(a[i] > minheap.top()) //如果当前元素大于容器中最小的元素,则将该元素push进容器
{
minheap.pop();
minheap.push(a[i]);//每次堆调整复杂度为O(logk)
}
}
}
return minheap.top();//返回堆顶元素,即为n个数中第k大的数(如果要返回前k个数,需将最后的minheap全部pop)
}
};
/*了解
用stl中make_heap函数,构建大顶堆,然后逐次输出堆顶元素(原理与用priority_queue相同,不过没有额外空间)
用时11ms
O(nlogn + klogn),O(1)
*/
#include <algorithm>
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
make_heap(nums.begin(),nums.end());//构建大顶堆,用nums存储,与下面区别就是节省了空间
for(int i = 0; i<k-1;i++)
{
pop_heap(nums.begin(),nums.end()); //将堆顶元素移至末尾,重新调整使(begin~end-1)的元素满足堆规律
nums.pop_back(); //移除末尾元素
}
return nums[0];
}
};
//用stl中sort函数,用时12ms
/*
#include <algorithm>
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
sort(nums.rbegin(), nums.rend()); //nums.rbegin()返回指向容器最后元素的逆向迭代器(因为sort默认按从小到大排序),
//sort将rbegin()指向位置当做第一个元素,故可以实现从大到小排序
return nums[k-1]; //第k个数,注意这里索引为k-1
}
};
*/
/*
//用stl中nth_element函数。?为什么没有sort()或者堆排序快
//用时14ms
#include <algorithm>
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
//nth_element(nums.begin(), nums.begin()+k-1, nums.end(),customMore);
nth_element(nums.begin(), nums.begin()+k-1, nums.end(),greater<int>()); //原理为快排
//这里直接用STL里的函数,比较函数设置为greater(默认为小数在前),注意中间(k-1)表示第k个最大的数
return nums[k-1]; //第k个数,注意这里索引为k-1
}
};
*/
/*
// 用自定义函数对象排序
struct
{
bool operator()(int a, int b) const
{
return a > b;
}
}customMore;
*/
相关:
Top K问题在数据分析中非常普遍的一个问题(在面试中也经常被问到),比如:
从20亿个数字的文本中,找出最大的前100个。
解决Top K问题有两种思路,
-
最直观:小顶堆(大顶堆 -> 最小100个数),该方法没有修改输入数据,且非常适合海量数据的输入,不用一次性读入内存,可以借助硬盘一边读一边处理,平均时间复杂度为O(nlogk)。
-
较高效:基于partition函数的解法,平均时间复杂度为O(n),但是会修改输入数组。
Quick Select(用快排中的partition函数)的目标是找出第k大元素,所以
-
若切分后的左子数组的长度 > k,则第k大元素必出现在左子数组中;
-
若切分后的左子数组的长度 = k-1,则第k大元素为pivot;
-
若上述两个条件均不满足,则第k大元素必出现在右子数组中。
quick select实质是分治法
分治法,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100*10000个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^6*4=4MB,一共需要101次这样的比较