思维导图:
一,TopK算法的运用
TopK的算法在我们的日常生活中可谓是大有用处,比如你在点外卖时外卖榜单上的销量前几名的筛选,富豪排行榜的榜单人物的筛选,游戏排位……等等领域都会有TopK算法的涉及。TopK问题的用处可大了!
二,TopK算法的实现
现在我先抛出一个问题。比如我要在一百万个数据里面寻找前五大的数据,那我一个小白会怎么做呢?毫无疑问,我会选择暴力求解!
思路:
1.将这一百万个数据排成大堆,然后将这个大堆删除k次那我每次在根节点得到的数据就是最大的前k个数。
这个想法非常美好,其实时间复杂度也不是很高!但是这个算法的空间复杂度却非常的高。一百万个数据就要建立一个400万个字节大小的堆。而且堆是不能在磁盘建立的只能在内存中建立,所以你的那点小小的内存会被占满!!!正是因为这个原因,这个方法就不能满足我们的需求!
2.TopK算法求解:建立一个只有k个数据的小堆,然后将这个堆的堆顶元素与剩下的N-k个数据进行比较。如果有数据大于堆顶元素那就代替堆顶元素进堆,然后通过向下调整算法将这个新的堆再次变成小堆!再与剩下的元素进行比较重复上面的操作!这样,这个TopK算法就可以解决上面算法空间复杂度太高的问题!
算法实现:
第一步:实现一个向下调整算法建堆
这个操作是为了建一个小堆。
这个代码要注意的点:
1.父节点与子节点的交换限制条件是子节点要在数组的范围内
即:n是数组的长度
while (child < n)
2.我们默认的要与父节点交换的节点是左节点,即:
int child = 2 * parent + 1;
但是左节点不一定比右节点小,所以在右节点小于左节点时,并且在右节点存在的情况下我们要将让比较小的右节点来与父节点进行交换,即:
if (child + 1 < n && a[child + 1] < a[child]) { child++; }
代码:
void swap(int* p1, int* p2)//交换函数
{
int temp = *p1;
*p1 = *p2;
*p2 = *p1;
}
Adjustdown(int* a, int n, int parent)//向下调整算法
{
int child = 2 * parent + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] < a[child])
{
child++;
}
if (a[child] < a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
第二步,实现TopK算法并将前k个数据进行打印:
在这一步骤中,我就是按照TopK算法的实现思路实现了这个算法。
这个算法的空间复杂度就是O(1),主要看创建堆的这一步:
int* KminHeap = (int*)malloc(sizeof(int) * k);//创建一个只有k个数据的小堆
时间复杂度是:O(n*logN).主要看这一步:
1.向下调整的时间复杂度是O(logN)。O(logN)*(n-k) = O(N*logN)
for (int i = k;i < n;i++) { if (a[i] > KminHeap[0])//如果后面的n-k个数据中有比KminHeap[0]大的数据就插入堆中 { KminHeap[0] = a[i]; Adjustdown(KminHeap, k, 0);//调整 } }
可以说这个算法完美的解决了第一个思路空间复杂度太大的问题!
代码:
void TopKPrint(int* a, int n, int k)
{
int* KminHeap = (int*)malloc(sizeof(int) * k);//创建一个只有k个数据的小堆
if (KminHeap == NULL)
{
perror("malloc fail");
return;
}
for (int i = 0;i < k;i++)//将数组的前k个数据插入到KminHeap堆中
{
KminHeap[i] = a[i];
}
Adjustdown(KminHeap, k, 0);//向下调整使KminHeap变成小堆
for (int i = k;i < n;i++)
{
if (a[i] > KminHeap[0])//如果后面的n-k个数据中有比KminHeap[0]大的数据就插入堆中
{
KminHeap[0] = a[i];
Adjustdown(KminHeap, k, 0);//调整
}
}
for (int i = 0;i < k;i++)//打印
{
printf("%d ", KminHeap[i]);
}
printf("\n");
}
第三步,创建数据
1. 我在创建数据时不想要一个固定的数据,所以用了一个可以随机生成数据的rand()函数:
a[i] = rand() % 100000;//生成随机数
这个代码理论上可以随机生成100000内的数据,但其实不行。它最多只能生成32767以内的数据(所以叫虚伪的随机数)。
2.要使用这个代码,那就需要一个生成随机数的种子:
srand(time(0));//生成随机数的种子
这个函数的参数是time(0),因为在计算机中只有时间time是一直在变化的!只有参数一直在变化rand()才能生成不同的数据。
代码:
int main()
{
srand(time(0));//生成随机数的种子
int n = 1000;
int* a = (int*)malloc(sizeof(int) * n);
if (a == NULL)
{
perror("malloc fail");
return;
}
for (int i = 0;i < n;i++)
{
a[i] = rand() % 100000;//生成随机数
}
TopKPrint(a, n, 5);
return 0;
}
写到这里我们的代码就写完了!
二, 更换数据读取位置
前面我们说,第一种排序的思路为什么不行来着?是不是因为它的空间复杂度太高了啊?还有一个限制就是因为我们只能在内存中建堆。而堆在物理上其实就是数组。那现在我在干什么?
int* a = (int*)malloc(sizeof(int) * n);
我是不是也在建立一个4*n字节大小的堆啊?当我的数据的数量很大时我是不是要完蛋啊。
很明显是的!所以我们要换一种读取数据的方式,我们要将读取的数据放到磁盘中。所以我在这里搞得第二种读取数据的方式就是从文件中读取数据。
2.1创建文件
创建文件的数据时仍然是像之前一样生成随机值。然后执行文件操作生成文件。
代码:
void CreatData(void)
{
int n = 10000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen fail");
return;
}
for (int i = 0;i < n;i++)
{
int x = rand() % 100000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
2.2TopKPrint实现
思路与之前使用数组实现TopK算法基本一致。
代码
void TopKPrint(int k)
{
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen fail");
return;
}
int* kminHeap = (int*)malloc(sizeof(int) * k);
if (kminHeap == NULL)
{
perror("malloc fail");
return;
}
for (int i = 0;i < k;i++)
{
fscanf(fout, "%d", &kminHeap[i]);
}
Adjustdown(kminHeap, k, 0);
while (!feof(fout))
{
int val = 0;
fscanf(fout, "%d", &val);
if (val > kminHeap[0])
{
kminHeap[0] = val;
Adjustdown(kminHeap, k, 0);
}
}
for (int i = 0;i < k;i++)
{
printf("%d ", kminHeap[i]);
}
}