这篇笔记记录求大数据topk的两种方法,分别是大小二叉堆法和快速分割法,下面依次详解这两种方法的过程。
1 大/小根堆法
利用大根堆过滤前top k小的数据**;小根堆过滤前top k大的数据**;
下面用大根堆求前k个小元素为例。
- 思想:
把大根堆堆顶的大值不断的淘汰,放入小值。先构建k个元素的大小堆(比如k=3,就是求数列中前3个元素),然后用数列中的元素依次与堆顶元素比较,如果大于或者小于堆顶元素,就进行出堆调整;最后再进行入堆调整。 - 过程
先用数列中前k个元素构建一个堆,比如构建一个大根堆。新元素与堆顶元素比较,大于堆顶元素就进行下一个元素;如果新元素比堆顶元素小,堆顶元素先出堆,然后将新元素加入到堆中。
代码
#include <iostream>
#include <vector>
#include <time.h>
#include <queue>
#include <functional>
using namespace std;
int main()
{
vector<int> vec;
srand(time(NULL));
for (int i = 0; i < 10; i++)
{
vec.push_back(rand() % 100);
cout << vec[i] << " ";
}
cout << endl;
// 求vec中值最小的前5个元素
priority_queue<int> maxheap;
int k = 5;
// 前5个元素入堆
for (int i = 0; i < 5; i++)
{
maxheap.push(vec[i]);
}
// 遍历剩余元素,直到最后
for (int i = 5; i < vec.size(); i++)
{
if (maxheap.top() > vec[i])
{
maxheap.pop();
maxheap.push(vec[i]);
}
}
// 输出结果
while (!maxheap.empty())
{
cout << maxheap.top() << " ";
maxheap.pop();
}
cout << endl;
求出序列中值最大的前k个元素
priority_queue<int, vector<int>, greater<int>> minheap;
// 前5个元素入堆
for (int i = 0; i < 5; i++)
{
minheap.push(vec[i]);
}
// 遍历剩余元素,直到最后
for (int i = 5; i < vec.size(); i++)
{
if (minheap.top() < vec[i])
{
minheap.pop();
minheap.push(vec[i]);
}
}
// 输出结果
while (!minheap.empty())
{
cout << minheap.top() << " ";
minheap.pop();
}
2 快排分割法
背景: 实际工作场景中经常遇到求topk的问题,比如找出最近人们搜索最多的商品,最多的应用等,多属于topk的问题,除了上面的利用大小根堆来求解topk问题之外,还可以利用快速排序思想求解topk问题。
**算法思想:**利用快速排序算法思想,判断快排分割函数每次返回的Pos值,如果pos值等于topk就直接返回,这样数组中从0到pos位置求出的就是前topk;如果pos大于或者小于topk,就继续求解。
算法步骤:
利用了快速排序的步骤。
算法性能分析:
平均时间复杂度:第一次找出一半的元素,第二次递归分割直达pos = K-1 ;一共N个元素,平均结果如下:
O(n)=N + N/2 +N/4 + … + 1 = 2N = O(n)
**最坏情况下时间复杂度:如果原始数列是有序的,比如从小到大的排列的,这时候求最大的top1时候,时间复杂度如下:
N +(N-1) + (N-2) +(N -3) + … + 1 = O(N2)=**O(n2)
代码
#include <iostream>
#include <functional>
#include <time.h>
using namespace std;
/*
* 功能:快排分割函数。每次递归时,将数组中的数据分割为两组,分别比val大的分成一组,比val小的分成一组。
*
* 参数:
* arr[]: 要分割的数组
* begin: 数组开始下标
* end : 数组的结束位置
*/
int Partition(int arr[], int begin, int end)
{
int val = arr[begin];
int i = begin;
int j = end;
while (i < j)
{
while (i < j && arr[j] > val)
{
j--;
}
// 如果找到右边的值大于begin,就进行下面的操作,把比val大的放在
if (i < j)
{
arr[i] = arr[j];
i++;
}
while (i < j && arr[i] < val)
{
i++;
}
if (i < j)
{
arr[j] = arr[i];
j--;
}
}
arr[i] = val;
return i;
}
/*
* 功能:求出前topk
*
* 参数:
* *arr : 求topk的数组
* begin: 数组开始下标
* end : 数组的结束位置
* k : 要求解的前topk。
*/
void SelectTopK(int *arr, int begin, int end ,int k)
{
int pos = Partition(arr, begin, end);
if (pos == k - 1) // 如果上面分割好的pos中间位置,刚好等于topk,直接返回。
{
return;
}
else if (pos > k - 1) // 假如返回 5 而要找topk=3,则继续快排分割。
{
SelectTopK(arr, begin, pos - 1, k);
}
else
{
SelectTopK(arr, begin, pos - 1, k);
}
}
int main()
{
int arr[] = { 64,45,52,80,66,68,0,2,18,75 };
int size = sizeof arr / sizeof arr[0];
// 假如求最小的topK,最后返回数组中的topk个元素。
int k = 3;
SelectTopK(arr, 0, size - 1,k);
for (int i = 0; i < k; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}