【算法】Top K问题

【算法】—— Top K问题

一、Top K问题描述

​ 给定一个很大的数组,要求找出最大或最小的前K个数

​ 为了体现出数组的大,和较为直观的观察结果,采用了文件存储大量数据,要取数组版的代码直接跳转到本文末尾

二、解题思路

1. 解题步骤

​ 这里以找出堆最大的前5个值为例子

数组

1.1 建堆

​ 利用向下调整算法将堆的前5个数值依次建成小根堆

调整成堆

堆的调整结果

2.2 选数

​ 遍历第K个以后的数,若是大于小根堆的堆顶,则进堆并向下调整

  1. 从k+1个数据开始遍历,遍历到值为6

遍历选数

  1. 6大于堆顶值,将6写入堆顶并向下调整成堆

选数调整
调整成堆

  1. 当所有数组遍历完毕后,则堆中的数据就是最大的前5个数

2. 算法效率

2.1 时间复杂度

  1. 创建数组并将k个元素复制到数组中需要执行k次
  2. 遍历k到结束位置的数组需要执行k次,每次遍历向下调整时最多交换 l o g 2 k log_2k log2k
  3. 所以一共执行 K + l o g 2 K × ( N − K ) ≈ N K+log_2K\times(N-K)\approx N K+log2K×(NK)N

​ 时间复杂度为: O ( n ) O(n) O(n)

2.2 空间复杂度

​ 空间只有创建的k个元素的数组,所以空间复杂度: O ( k ) O(k) O(k)

三、代码实现(参数为文件)

​ 注意:向下调整函数的实现在下面单独给出

void PrintTopK(const char* filename, int k)
{
    //获取文件
	FILE* pf = fopen(filename, "r");
	if (pf == NULL)
	{
		perror("fopen fail");
		exit(-1);
	}
	
    //创建数组
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//读取文件前k个数
	int i = 0;
	for (i = 0; i < k; i++)
	{
		fscanf(pf, "%d ", &minHeap[i]);
	}

	//向下调整建堆
	for (i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDown(minHeap, k, i);	//调用向下调整函数
	}

	//读取k到N个数
	int data = 0;
	while (fscanf(pf, "%d ", &data) != EOF)
	{
		if (minHeap[0] < data)
		{
			minHeap[0] = data;
			AdjustDown(minHeap, k, 0);
		}
	}
	
	//打印最大前k个数
	for (i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}

	free(minHeap);
	fclose(pf);
}

向下调整函数

void AdjustDown(HPDataType* data, int size, int parent)
{
	int minchild = parent * 2 + 1;
	while (minchild < size)
	{
		if (minchild + 1 < size && data[minchild] > data[minchild + 1])
		{
			minchild++;
		}

		if (data[minchild] < data[parent])
		{
			Swap(&data[minchild], &data[parent]);
			parent = minchild;
			minchild = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

四、运行实例

1. 数组文件

​ 创建一个txt文件,并写一个函数在文件写入0到99999共100000个整数,中间使用空格隔开,没有换行符

数组文件开头

数组文件尾

2. 函数调用

​ 将数组文件传进去,并令k为10,打印最大的前10个数字

int main()
{
    char* filename = "array.txt";
    PrintTopK(filename, 10);
}

3. 运行结果

​ 打印出了文件中最大的10个数字

运行结果

五、相关问题

1. 建堆和选数的原则

​ 建堆原则:

  1. 选出最大的前k个数建小根堆:小根堆堆顶最小,选数时选择比堆顶还小的一定比堆的所有节点小,不入堆
  2. 选出最小的前k个数建大根堆:大根堆堆顶最大,选数时选择比堆顶还大的一定比堆的所有节点大,不入堆

​ 选数原则:

  1. 选最大前k个数选择比堆顶大的,小根堆堆顶最小,被比较的数若大于堆顶则入堆,淘汰最小值
  2. 选最大前k个数选择比堆顶小的,大根堆堆顶最大,被比较的数若小于堆顶则入堆,淘汰最大值

2. 调整方式的选择

​ 将数组构建成堆时,可以使用向上调整或向下调整,但是我们选择的是向下调整,因为向下调整时间效率更高

1. 向上调整

​ 将数组第1个元素作为一个堆,从第2个元素到第n-1个元素依次进行向上调整。

int i = 0;
for (i=1; i<n; i++)
{
    AdjustUp(arr, i);	//从1到n-1进行调整
}

​ 向上调整的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)

​ 所以使用向上调整建堆的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

2. 向下调整

  1. 向下调整是从后往前调整,先将后面构成堆,再调整上面的节点
  2. 以叶子节点作为根进行向下调整是完全没有必要的,叶子节点没有子节点,所以对最后一个叶子节点的父节点开始向下调整
  3. 最后一个节点下标是n-1,它的父节点为 (n-1-1) / 2
int i = 0;
for (i=(n-2)/2; i>=0; i--)
{
    AdjustDown(arr, n, i);	//从 (n-2)/2 到 0 进行调整
}

​ 时间复杂度计算

向下调整时间复杂度

则需要移动节点的步数为

T n = 2 0 × ( h − 1 ) + 2 1 × ( h − 2 ) + 2 2 × ( h − 3 ) + . . . + 2 h − 2 × 1 T_n=2^0\times(h-1)+2^1\times(h-2)+2^2\times(h-3)+...+2^{h-2}\times1 Tn=20×(h1)+21×(h2)+22×(h3)+...+2h2×1

这是一个等差数列乘以一个等比数列,使用裂项相消法进行化简

2 T n = 2 1 × ( h − 1 ) + 2 2 × ( h − 2 ) + 2 3 × ( h − 3 ) + . . . + 2 h − 1 × 1 2T_n=2^1\times(h-1)+2^2\times(h-2)+2^3\times(h-3)+...+2^{h-1}\times1 2Tn=21×(h1)+22×(h2)+23×(h3)+...+2h1×1

2 T n − T n = − h + 2 0 + 2 1 + 2 2 + . . . + 2 h − 1 + 2 h − 2 × 1 2T_n-T_n=-h+2^0+2^1+2^2+...+2^{h-1}+2^{h-2}\times1 2TnTn=h+20+21+22+...+2h1+2h2×1

T n = − h + 2 h − 1 T_n=-h+2^h-1 Tn=h+2h1

因为 n = 2 h − 1 n = 2^h-1 n=2h1 h = l o g 2 ( n + 1 ) h=log_2{(n+1)} h=log2(n+1)

T n = n − l o g 2 ( n + 1 ) ≈ n T_n=n-log_2(n+1)\approx n Tn=nlog2(n+1)n

向下调整的时间复杂度为 O ( n ) O(n) O(n)

因此向下调整建堆的时间效率比向上调整建堆的时间效率更高

六、参数为数组的代码实现

void PrintTopK(const int* arr, int size, int k)
{
    //创建堆数组
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//复制前k个数到堆数组
	int i = 0;
	for (i = 0; i < k; i++)
	{
		minHeap[i] = arr[i];
	}

	//向下调整建堆
	for (i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDown(minHeap, k, i);	//调用向下调整函数
	}

	//遍历k到N个数
	for (i = k; i < size; i++)
    {
        if (arr[i] > minHeap[0])
        {
            minHeap[0] = arr[i];
            AdjustDown(minHeap, k, 0);
        }
    }
	
	//打印最大前k个数
	for (i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}

	free(minHeap);
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值