文章目录
前言
Top-K问题也算是面试中或者题目中常见的一类问题,本篇文章将会使用堆结构的方法来解决Top-K问题。
一、什么是Top-K问题
所谓Top-K问题就是从众多数据中找到前K个最大值或者最小值。这个问题不是简单的排个序就能完成的,因为Top-K问题往往设计到的数据量都非常非常多,具体有多多我也不敢定论,反正就是在非常非常多的数据中找出前K个最值。
二、回顾堆结构
1.堆的概念和性质
所谓堆,就是一种特殊的完全二叉树。在堆当中,任何一个结点的值都要比其孩子要大或者小,任意结点都比其孩子大的叫做大堆(或大根堆),任意结点都比其孩子小的叫做小堆(或小根堆)。
2.堆在数组中的存储
由于堆是一个完全二叉树,而完全二叉树从根结点到最后一个结点之间是没有空值的,是一种非常连续的结构。所以我们大多数情况下都是使用顺序存储在数组当中。
堆,或者说二叉树在数组中都是一层一层、从左往右存储的。根结点的下标为 0 。
需要注意的是,根据上图,我们可以找到一个规律,这个规律在堆当中是蛮重要的。假设某一个结点的下标是 i ,那么它的父结点(假如存在)的下标就是 (i - 1) / 2 ,它的左孩子(假如存在)的下标就是 i * 2 + 1 ,它的右孩子(假如存在)的下标就是 i * 2 + 2。所以在数组当中,我们可以根据以上规律很快的找到任意结点的父结点或者子结点。
三、算法思想
因为堆的根结点是堆的最值,比较特殊,所以在涉及到堆的问题中往往要对根结点进行分析。使用堆解决Top-K问题其实只需要知道两个步骤,1.建什么堆 ,2.调整堆结构。
1.建什么堆
我们既然要找到前k个最值,那么我们就需要建立一个大小为k的堆来存放这几个最值。那么我们要建立大堆还是小堆呢?假设我们要找前k个最大值,我们需要建立一个小堆,因为堆顶存放的是k个最大值中的最小值,那个最不配为最大值的数,我们在遍历数据的时候要不断与堆顶元素进行比较,遇到比堆顶元素更符合要求的数就要替换掉堆顶元素,然后再将其调整到它应该在的位置。
建堆就是建使堆顶元素尽量不符合要求的堆。
2.调整堆结构
在让新数据覆盖到根结点时,此时的堆不一定满足堆结构,需要进行调整。让父结点不断与其孩子结点比较,小堆让最小值到父结点,大堆让最大值到父结点,没有发生交换说明已经符合堆的结构,不需要再调整了。
首先将前k个数据存放到堆中并使其成堆结构,然后遍历数据,依次与堆顶元素比较,比堆顶元素更符合要求的则覆盖堆顶元素,再使其调整到正确位置上,重复此操作,将所有数据遍历完了以后,堆中存放的就是前k个最值,而堆顶元素就是第k个最值。
四、代码实现
代码中有部分属于堆实现的函数,在此不过多展开。感兴趣的可以看看堆的实现。
1.库函数头文件及声明
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
// 宏定义数据类型
typedef int HPDataType;
// 堆结构体
typedef struct Heap
{
// 指向数组的指针
HPDataType* a;
// 堆的有效数据个数
int size;
// 堆的容量
int capacity;
}HP;
// 初始化
void HeapInit(HP* php);
// 销毁
void HeapDestroy(HP* php);
// 向上调整
void AdjustUp(HPDataType* a, int child);
// 向下调整
void AdjustDown(HPDataType* a, int size, int parent);
// 交换
void Swap(HPDataType* a, HPDataType* b);
2.其他函数实现
// 堆的初始化
void HeapInit(HP* php)
{
// php为指向堆结构体指针,为空说明传参错误
assert(php);
// 指向数组的指针置空
php->a = NULL;
// 堆的有效数据个数初始为0
php->size = 0;
// 堆的容量初始为0
php->capacity = 0;
}
// 堆的销毁
void HeapDestroy(HP* php)
{
assert(php);
// 释放数组空间
free(php->a);
// 指针置空
php->a = NULL;
// 堆的大小回归默认
php->size = 0;
// 堆的容量回归默认
php->capacity = 0;
}
// 交换函数
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
// 向上调整
void AdjustUp(HPDataType* a, int child)
{
// 父结点的下标
int parent = (child - 1) / 2;
// 待调整的结点没有到根结点
while (child > 0)
{
// 待调整的结点比父结点要小
if (a[child] < a[parent])
{
// 交换
Swap(&a[child], &a[parent]);
// 更新待调整结点的下标
child = parent;
// 更新父结点下标
parent = (parent - 1) / 2;
}
// 不需要调整说明已经形成堆结构,直接退出
else
break;
}
}
// 向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
// 父结点的左孩子
int child = parent * 2 + 1;
// 如果左孩子结点在有效数据内
while (child < size)
{
// 找出左右孩子中的小值
if (child + 1 < size && a[child + 1] < a[child])
++child;
// 子结点比父结点要小
if (a[child] < a[parent])
{
// 交换
Swap(&a[child], &a[parent]);
// 更新父结点下标
parent = child;
// 更新子结点下标
child = parent * 2 + 1;
}
// 如果不需要调整,说明已经是堆结构,直接跳出
else
break;
}
}
3.在文件中造数据函数
void CreateNDate(const char* filename)
{
// 造的数据个数
int n = 10000;
// 随机时间种子
srand(time(0));
// 文件名
const char* file = filename;
// 以写方式打开文件,没有则创建
FILE* fin = fopen(file, "w");
// 没打开
if (!fin)
{
// 弹出反馈
perror("fopen error");
exit(-1);
}
// 造数据
for (size_t i = 0; i < n; ++i)
{
// 随机值
int x = (rand() + i) % 1000000;
// 打印到文件当中
fprintf(fin, "%d\n", x);
}
// 关闭文件
fclose(fin);
}
4.Top-K函数
void TopK(const char* filename, int k)
{
// 以读的方式打开文件
FILE* file = fopen(filename, "r");
// 没打开
if (!file)
{
// 弹出反馈
perror("fopen fail");
exit(-1);
}
// 创建堆
HP hp;
// 初始化堆
HeapInit(&hp);
// 建立k个大小为存储的数据类型的空间
HPDataType* minheap = (HPDataType*)malloc(sizeof(HPDataType) * k);
// 将文件前k个元素存入到堆当中
for (int i = 0; i < k; ++i)
{
// 从文件中读取1个数据存放到小堆当中
fscanf(file, "%d", &minheap[i]);
// 每读取一个数据向上调整
AdjustUp(minheap, i);
}
// 存储文件数据
HPDataType x = 0;
// 文件没读取到末尾
while (fscanf(file, "%d", &x) != EOF)
{
// 如果读取到的数据比小堆中的最小数据要大(更符合要求)
if (x > minheap[0])
{
// 将数据存入堆中
minheap[0] = x;
// 新的堆顶元素向下调整
AdjustDown(minheap, k, 0);
}
}
// 打印堆中元素
for (int i = 0; i < k; ++i)
printf("%d ", minheap[i]);
printf("\n");
}
5.主函数
int main()
{
// 这个函数在执行过一次后需要注释掉,避免不断更改文件数据
CreateNDate("data.txt");
// 前10大的数据
TopK("data.txt", 10);
return 0;
}
CreateNData函数在执行过一次后需要注释掉!!
6.结果演示
无法验证这是否是真正正确的答案,所以我们往文件里面手动改几个数字,这个数字是刚刚随机数所不能超过的数字。我们再来演示一下。
嗯,完美。
提一嘴
代码是实现小堆的,所以找出的是前k个最大值。如果想要最小值,可以在调整函数里面将部分 < 改成 > 即可,至于是哪部分就不需要我多说了。
我们在向堆中插入k个数据的时候,我们使用的是向上调整函数建立的堆,当然,建立堆也可以使用向下调整函数,至于怎么更改,我也不多说,理解了就非常简单。
总结
本篇文章到这里就结束了,我们使用了堆的结构来解决Top-K问题。希望本篇文章对你有所帮助,如果发现错误,请读者不吝赐教。谢谢。