运用堆结构来解决Top-K问题


前言

  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问题。希望本篇文章对你有所帮助,如果发现错误,请读者不吝赐教。谢谢。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值