【LeetCode & 剑指offer刷题】查找与排序题2:40 最小的k个数(对应Kth Largest Element in an Array)
【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
using namespace std;
class Solution
{
public:
vector GetLeastNumbers_Solution(vector input, int k)
{
if(k<=0 || k > input.size()) return vector(); //处理非法输入
nth_element(input.begin(), input.begin()+k-1, input.end());
vector 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)。
参考:被忽视的 partition 算法
*/
#include
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模板,从大到小排序(默认从小到大,左结点<父结点<根结点)
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
k
th 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.
C++
暴力法:直接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
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
#inlude
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
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
class Solution
{
public:
int findKthLargest(vector& 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
class Solution
{
public:
int findKthLargest(vector& 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()); //原理为快排
//这里直接用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问题的两种解决思路
Top K问题在数据分析中非常普遍的一个问题(在面试中也经常被问到),比如:
从20亿个数字的文本中,找出最大的前100个。
解决Top K问题有两种思路,
-
最直观:小顶堆(大顶堆 -> 最小100个数), 该方法没有修改输入数据,且非常适合海量数据的输入,不用一次性读入内存,可以借助硬盘一边读一边处理,平均时间复杂度为O(nlogk)。
-
较高效:基于partition函数的解法, 平均时间复杂度为O(n),但是会修改输入数组。
Quick Select(用快排中的partition函数)的目标是找出第k大元素,所以
-
若切分后的左子数组的长度 > k,则第k大元素必出现在左子数组中;
-
若切分后的左子数组的长度 = k-1,则第k大元素为pivot;
-
若上述两个条件均不满足,则第k大元素必出现在右子数组中。
https://www.cnblogs.com/en-heng/p/6336625.html
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次这样的比较
https://blog.csdn.net/zyq522376829/article/details/47686867
posted @
2019-01-05 20:07 wikiwen 阅读(
...) 评论(
...) 编辑 收藏