Topk问题的三种求解方法

什么是Topk问题

其实顾名思义,这个问题也就是在N个数中找出前k个最值。在我们的日常生活中,很多地方都有Topk问题的影子,例如我们在点外卖时,总会说这家店是某某市的多少名,其实这些都是用Topk问题的解决方法得出来的。

方法一:堆排序法

这也是最容易想到的一种方法:我们可以将N个数排成有序的,然后输出前k个最值,而在我们已学过的排序算法中,堆排序的时间复杂度又是最快的(O(n*logn)),因此我们选择了堆排序。

//交换函数
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
//堆的向下调整(小堆)
void AdjustDown(int* a, int n, int parent)
{
	//child记录左右孩子中值较小的孩子的下标
	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;
		}
	}
}
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
	*returnSize = k;
	int i = 0;
	//建小堆
	for (i = (arrSize - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, arrSize, i);
	}
	//排降序
	int end = arrSize - 1;
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, end, 0);
		end--;
	}
	//将最大的k个数存入数组
	int* retArr = (int*)malloc(sizeof(int)*k);
	for (i = 0; i < k; i++)
	{
		retArr[i] = arr[i];
	}
	return retArr;//返回最大的k个数
}

时间复杂度:O(N+NlogN) 空间复杂度:O(N),能否将其再优化了?肯定是可以的,下面看方法二。

方法二:把N个数建堆,取出前k个

这种方法其实也不难想到,我们利用了堆的性质:堆的0号位一定是最值,小堆则为最小值,大堆则为最大值。

注意点:
1.取出数据后要让其与最后的元素替换,因为你已经取出这个元素了,所以不需要它了,这时让它去堆尾,不让它算入堆的个数中就行了。为什么要这样做了,因为这样既保证了堆的结构,也相当于把这个取出来的数删除了
2. 如果在取到堆顶数据后直接删除数据,那么就要重新建堆了。正确的做法应该是上面所说的方法,因为那样只要进行一次向下调整,就可以保证堆的结构了。要知道建堆的复杂度为O(N),而一次向下调整的复杂度仅为O(logn),这样大大提升了效率。

//交换函数
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
//堆的向下调整(大堆)
void AdjustDown(int* a, int n, int parent)
{
	//child记录左右孩子中值较大的孩子的下标
	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;
		}
	}
}
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
	*returnSize = k;
	int i = 0;
	//建大堆
	for (i = (arrSize - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, arrSize, i);
	}
	//将最大的k个数存入数组
	int* retArr = (int*)malloc(sizeof(int)*k);
	int end = arrSize - 1;
	for (i = 0; i < k; i++)
	{
		retArr[i] = arr[0];//取堆顶数据
		Swap(&arr[0], &arr[end]);//交换堆顶数据与最后一个数据
		//进行一次向下调整,不把最后一个数据看作待调整的数据,所以待调整数据为end=arrSize-1
		AdjustDown(arr, end, 0);
		end--;//最后一个数据的下标改变
	}
	return retArr;//返回最大的k个数
}

时间复杂度:O(n+k*logn),空间复杂度:O(n),那么问题来了,万一n是一个特别大的数字了,那效率依旧不是很高,有没有更快的方法了?下面我们来看看方法三。

方法三:建一个k个数的堆

我们以找出最大的k个数为例,该方法为:先建一个k个数的小堆,然后将数组中n-k个元素依次与堆顶的元素比较,若比堆顶元素大,则将堆顶元素删除,然后将这个数插入到堆中,这儿要注意:插入这个数到堆中的时候,要使用向上排序算法保证堆的结构不被破坏。到最后,堆里面的k个数就是最大的k个数了。

那么问题来了,上面的例子中为什么不用大堆了,不是选最大的k个数吗?其实这很容易理解,我们假设一下,如果建一个大堆,万一堆顶的数据就是n个数中最大的那个,那么不论怎么比较,你后面的n-k个元素都没法进堆,因此要用小堆来解决这个问题。

void PrintTopK(int* a, int n, int k)
{
	Hp hp;
	HpInit(&hp, a, k);
	int i = k;
	for (i = k; i < n; i++)
	{
		if (a[i]>HpTop(hp))
		{
			HpPop(&hp);
			HpPush(&hp, a[i]);
		}
	}
	//你此时数据改变之后是在堆里面的,你打印a没用啊
	//for (i = 0; i < k; i++)
	//{
	//	printf("%d ", a[i]);
	//}
	for (i = 0; i < k; i++)
	{
		printf("%d ", HpTop(hp));
		HpPop(&hp);
	}
	printf("\n");
}

时间复杂度:O(k+n*logk) 空间复杂度:O(n) 与之前两种方法对比,这种方法大大提高了效率。

  • 36
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 41
    评论
BFPRT算法是一种解决Top-K问题算法,其中K为要找的第K大的元素。该算法的时间复杂度为O(n)。 BFPRT算法的基本思想是通过划分算法来找到中位数,然后根据中位数将数组分成两部分,只需要在一部分中查找Top-K,另一部分不用查找。 以下是BFPRT算法c语言实现: ```c #include <stdio.h> #include <stdlib.h> #define SWAP(a, b) {int temp = a; a = b; b = temp;} // 求中位数 int median(int arr[], int left, int right) { int center = (left + right) / 2; if (arr[left] > arr[center]) SWAP(arr[left], arr[center]) if (arr[left] > arr[right]) SWAP(arr[left], arr[right]) if (arr[center] > arr[right]) SWAP(arr[center], arr[right]) SWAP(arr[center], arr[right - 1]) return arr[right - 1]; } // 划分函数 int partition(int arr[], int left, int right, int pivot) { int i, j; for (i = left, j = right - 2;;) { while (arr[++i] < pivot); while (arr[--j] > pivot); if (i < j) SWAP(arr[i], arr[j]) else break; } SWAP(arr[i], arr[right - 1]) return i; } // BFPRT算法 int bfprt(int arr[], int left, int right, int k) { if (left + 1 == right) return arr[left]; int pivot = median(arr, left, right); int mid = partition(arr, left, right, pivot); int num = mid - left + 1; if (k == num) return arr[mid]; else if (k < num) return bfprt(arr, left, mid, k); else return bfprt(arr, mid + 1, right, k - num); } int main() { int arr[] = {3, 4, 2, 1, 5, 6, 8, 7, 9}; int len = sizeof(arr) / sizeof(int); int k = 5; int kth = bfprt(arr, 0, len, k); printf("The %dth number is: %d\n", k, kth); return 0; } ``` 该算法的核心是BFPRT划分算法,该算法是一个递归算法,每次递归的时间复杂度为O(n),因此总的时间复杂度为O(nlogn)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个数学不怎么好的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值