我们解决这个问题最简单的方法就是将序列直接进行一次排序,再进行遍历即可获得TopK,但是我们只需要获得TopK的数据,并不需要获得所有元素的序列,这样的方法浪费了很多时间复杂度,因此下面介绍两种更加优秀的方法来解决TopK问题
基于堆排序
流程
- 共n个数,先建立一个k个数的小根堆
- 初始化i=k+1
- 如果i>n则结束否则进行下面操作
- 若a[i]>堆顶元素则替换堆顶,shiftdown操作让堆顶元素到达正确位置,若a[i]<=堆顶元素则直接进行第二步
- i++
- 结束后堆顶就是第k大的数字,堆里就是倒序的前k大的数
时间复杂度分析
- 建k个数的堆:O(k)
- 后面数据进行比较调整
- 最坏情况:k+1开始每个数都比堆顶大,遍历原数组中剩余元素需要O(n-k)的时间复杂度,每个剩余的元素进行shiftdown操作,k个元素有Logk层则每个数调整Logk次时间复杂度为O(Logk)
- 最好情况:后面的都不需要调整了,前k个数正好是堆Topk大的数,因此只需要遍历原数组中剩余元素,复杂度为O(n-k)
- 总体时间复杂度:
- 最坏:O(n+(n-k)*Logk)
- 最好:O(n)
- 空间复杂度:空间复杂度:O(logn),递归使用栈空间的空间代价的期望为 O(logn)。
代码
void TestTopK(int* a, int n, int k)
{
//建堆,最后一个非叶子结点的下标是(k-1-1)/2。
for(int i=(k-1-1)/2; i>=0; i--)
{
ShiftDown(a, k, i);
}
//调整
for(int i=k+1; i<n; i++)
{
if(a[i] > a[0])
{
Swap(&a[0], &a[i]);
ShiftDown(a, k, 0);
}
}
}
基于快速排序
流程
- 基于快速排序,寻找TopK元素,实际上我们找的就是nums[k-1]的数
- 进行一次快排的partition(划分),若pivot所在的index>k-1则说明nums[index]>nums[k-1]则需要在pivot左边继续进行一次partition,若<k-1则在右边找,若index=k-1说明找到了
- 重复第二步直到index=k-1,nums[k-1]就是第TopK大的数
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(logn),递归使用栈空间的空间代价的期望为 O(logn)。
代码
class Solution {
public:
int quickSelect(vector<int>& a, int l, int r, int index) {
int q = randomPartition(a, l, r);
if (q == index) {
return a[q];
} else {
return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}
}
inline int randomPartition(vector<int>& a, int l, int r) {
int i = rand() % (r - l + 1) + l;
swap(a[i], a[r]);
return partition(a, l, r);
}
inline int partition(vector<int>& a, int l, int r) {
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x) {
swap(a[++i], a[j]);
}
}
swap(a[i + 1], a[r]);
return i + 1;
}
int findKthLargest(vector<int>& nums, int k) {
srand(time(0));
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
};