topK的3种解法

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了topK的3种解法相关的知识,希望对你有一定的参考价值。

参考技术A 思路:

(1)可以避免对所有数据进行排序,只排序部分;

(2) 冒泡排序是每一轮排序都会获得一个最大值,则K轮排序即可获得TopK。

时间复杂度空间复杂度:(1)时间复杂度:排序一轮是O(N),则K次排序总时间复杂度为:O(KN)。(2)空间复杂度:O(K),用来存放获得的topK,也可以O(1)遍历原数组的最后K个元素即可。

思路:

(1)堆:分为大顶堆(堆顶元素大于其他所有元素)和小顶堆(堆顶其他元素小于所有其他元素)。

(2)我们使用小顶堆来实现。

(3) 取出K个元素放在另外的数组中,对这K个元素进行建堆。

(4) 然后循环从K下标位置遍历数据,只要元素大于堆顶,我们就将堆顶赋值为该元素,然后重新调整为小顶堆。

(5) 循环完毕后,K个元素的堆数组就是我们所需要的TopK。

时间复杂度与空间复杂度:

(1)时间复杂度:每次对K个元素进行建堆,时间复杂度为:O(KlogK),加上N-K次的循环,则总时间复杂度为O((K+(N-K))logK),即O(NlogK),其中K为想要获取的TopK的数量N为总数据量。

(2)空间复杂度:O(K),只需要新建一个K大小的数组用来存储topK即可

思路:

(1)比如有10亿的数据,找处Top1000 ,我们先将10亿的数据分成1000份,每份100万条数据。

(2) 在每一份中找出对应的Top 1000,整合到一个数组中,得到100万条数据,这样过滤掉了999%%的数据。

(3) 使用快速排序对这100万条数据进行”一轮“排序,一轮排序之后指针的位置指向的数字假设为S,会将数组分为两部分,一部分大于S记作Si,一部分小于S记作Sj。

(4) 如果Si元素个数大于1000,我们对Si数组再进行一轮排序,再次将Si分成了Si和Sj。如果Si的元素小于1000,则我们需要在Sj中获取1000-count(Si)个元素的,也就是对Sj进行排序(5)如此递归下去即可获得TopK。

时间复杂度与空间复杂度:

(1)时间复杂度:一份获取前TopK的时间复杂度:O((N/n)logK)。则所有份数为:O(NlogK),但是分治法我们会使用多核多机的资源,比如我们有S个线程同时处理。则时间复杂度为:O((N/S)logK)。之后进行快排序,一次的时间复杂度为:O(N),假设排序了M次之后得到结果,则时间复杂度为:O(MN)。所以 ,总时间复杂度大约为O(MN+(N/S)logK) 。

(2)空间复杂度:需要每一份一个数组,则空间复杂度为O(N)。

堆排序与海量TopK问题

在这里插入图片描述

系列文章目录



在这里插入图片描述

前言

排序算法是个老生常谈的问题,笔试要考,面试也问,不过翻来覆去也就那几个花样吧。大概理解一下各个算法的原理,记下表格里的数据,然后再试试手撕代码,基本上就没问题了。
在这里插入图片描述
从表格里可以看出,堆排序是一个时间和空间复杂度都比较优秀的算法,至于它的原理,看懂是肯定能轻易看懂的,但是我总觉得如果你不自己亲手写一遍,就很容易忘记。并且,用递归的话,代码也是很简短的。


一、堆排序

1.什么是堆

在这里插入图片描述
堆(heap)是一种数据结构,也被称为优先队列(priority queue)。队列中允许的操作是先进先出(FIFO),在队尾插入元素,在队头取出元素。而堆也是一样,在堆底插入元素,在堆顶取出元素,但是堆中元素的排列不是按照到来的先后顺序,而是按照一定的优先顺序排列的。这个优先顺序可以是元素的大小或者其他规则。
而二叉堆是一种特殊的堆,它是完全二元树(二叉树)或者是近似完全二元树(二叉树)。二叉堆有两种:最大堆和最小堆。最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。如下图。

二、堆排序原理

堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。它的关键在于建堆和调整堆。步骤主要如下:

  1. 创建一个堆;
  2. 把堆首(最大值)和堆尾互换;
  3. 把堆的尺寸缩小1,并调整堆,把新的数组顶端数据调整到相应位置;
  4. 重复步骤 2,直到堆的尺寸为1,此时排序结束。

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
在这里插入图片描述
当然,光看文字肯定不能很直观地理解,我们跟着图示来学习吧。现在,我们有一个待排序的数组 {2, 4, 3, 7, 5, 8},我们通过构建最大堆的方法来排序。
在这里插入图片描述
对于建堆时间复杂度是O(N):
在这里插入图片描述

1.堆排序的实现

代码如下:

void swap(int* left, int* right)
{
	int tmp = 0;
	tmp = *left;
	*left = *right;
	*right = tmp;
}
void AdjustDown(int* arr, int sz, int parent)
{
	int child = 2 * parent + 1;
	while (child < sz)
	{
		if (child + 1 < sz&&arr[child + 1] > arr[child])
		{
			child++;
		}
		if (arr[child]>arr[parent])
		{
			swap(&arr[child], &arr[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}
//排升序,建大堆
void HeapSort(int *arr, int sz)
{
	//建堆
	int i = (sz - 1 - 1) / 2;
	for (i = (sz - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, sz, i);
	}
	int end = sz - 1;	
	while (end > 0)
	{
		//选出次大的
		swap(&arr[0], &arr[end]);
		//最后一个不用交换,所以为n-1个数为end
		AdjustDown(arr, end, 0);
		end--;
	}
}

三、海量TopK问题

最小的K个数
指Offer有这样一道题,求最小的K个数,题目描述:输入n个整数,找出其中最小的K个数。例如输入 4,5,1,6,2,7,3,8 这8个数字,则最小的4个数字是 1,2,3,4。
而在面试的时候,我们也可能遇到这样的问题:有一亿个浮点数,如何找出其中最大的10000个?
这类问题我们把称为TopK问题:指从大量数据(源数据)中获取最大(或最小)的K个数据。
在这里插入图片描述

1.第一种解法

建立一个大小为arrSize的堆,用进堆和出堆HeapPop和HeapTop两种方法,
代码如下:

void HeapInit(struct Heap* hp, HPDataType* a, int n)     //初始化堆
{
	assert(hp);
	hp->a = (HPDataType*)malloc(sizeof(HPDataType)*n);
	if (hp->a == NULL)
	{
		printf("malloc fail!\\n");
		exit(-1);
	}
	memcpy(hp->a, a, sizeof(HPDataType)*n);
	hp->size = n;
	hp->capacity = n;
	int i = 0;
	for (i = (hp->size - 2) / 2; i >= 0; i--)
	{
		AdjustDown(hp->a, hp->size, i);
	}
}
void HeapPop(struct Heap* hp)
{
	assert(hp);
	assert(hp->size > 0);
	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--; //删除掉堆底数据
	AdjustDown(hp->a, hp->size, 0);
}
HPDataType HeapTop(struct Heap* hp)
{
	assert(hp);
	assert(hp->size != 0);
	return hp->a[0];
}
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
	struct Heap hp;
	HeapInit(&hp, arr, arrSize);
	int* ret = (int*)malloc(sizeof(int)*k);
	for (int i = 0; i<k; i++)
	{
		ret[i] = HeapTop(&hp);
		HeapPop(&hp);
	}
	*returnSize = k;
	return ret;
}

2.第二种解法

我们可以先取下标 0~k-1 的局部数组,用它来维护一个大小为K的数组,然后遍历后续的数字,进行比较后决定是否替换。这时候堆排序就派上用场了。我们可以将前K个数字建立为一个最大堆,如果是要取最大的K个数,则在后续遍历中,将数字与最大堆的堆顶数字进行比较,若比它小,则进行替换,然后再重新调整为最大堆。整个过程直至所有数字遍历完为止。时间复杂度为O(n*log₂K),空间复杂度为K。
代码如下:

#define _CRT_SECURE_NO_WARNINGS   1
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
#include<stdio.h>
#include<stdlib.h>
void Swap(int* left, int* right)
{
	int tmp = 0;
	tmp = *left;
	*left = *right;
	*right = tmp;
}
void AdjustDown(int* a, int n, int parent)
{
	int child = 2 * parent + 1;
	while (child<n)//(n > 0)
	{
		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* smallestK(int* arr, int arrSize, int k, int* returnSize)
{
	if (k == 0)
	{
		*returnSize = 0;
		return NULL;
	}
	int* arrRet = (int*)malloc(sizeof(int)*k);
	int i = 0;
	//前面k个数建立大堆
	for (i = 0; i<k; i++)
	{
		arrRet[i] = arr[i];
	}
	int j = 0;
	for (j = (k - 1 - 1) / 2; j >= 0; j--)
	{
		AdjustDown(arrRet, k, j);
	}
	//后面n-k个数,比堆顶的小,进堆,在调堆
	for (i = k; i<arrSize; i++)
	{
		if (arrRet[0]>arr[i])
			arrRet[0] = arr[i];
		AdjustDown(arrRet, k, 0);
	}
	*returnSize = k;
	return arrRet;
}
int main()
{
	return 0;
}

关键:
相似的TopK问题还有:

  1. 有10000000个记录,这些查询串的重复度比较高,如果除去重复后,不超过3000000个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。请统计最热门的10个查询串,要求使用的内存不能超过1GB。
  2. 有10个文件,每个文件1GB,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。按照query的频度排序。
  3. 有一个1GB大小的文件,里面的每一行是一个词,词的大小不超过16个字节,内存限制大小是1MB。返回频数最高的100个词。
  4. 提取某日访问网站次数最多的那个IP。
  5. 10亿个整数找出重复次数最多的100个整数。
  6. 搜索的输入信息是一个字符串,统计300万条输入信息中最热门的前10条,每次输入的一个字符串为不超过255B,内存使用只有1GB。
  7. 有1000万个身份证号以及他们对应的数据,身份证号可能重复,找出出现次数最多的身份证号。
    等等…

总结

以上就是今天要讲的内容,本文仅仅简单介绍了堆中堆排序和热门问题TopK的解法,而堆提供了大量能使我们快速便捷地处理数据的函数和方法,我们务必掌握。另外,如果有需要源码的私信我即可。还有,如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。
在这里插入图片描述

以上是关于topK的3种解法的主要内容,如果未能解决你的问题,请参考以下文章

堆排序与海量TopK问题

TopK高频重复 算法题

Spark TopK问题解法

[经典] 在未排序数组中返回topK大的数

求解TopK问题的三种境界(漫画版)

堆排序和TopK问题