- https://github.com/weitingyuk/LeetCode-Notes-Waiting/blob/main/2021-02-17/TopK.md
- https://segmentfault.com/a/1190000021109127
个人整理,有一些也是个人理解,并不存在标准答案
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
前缀树的3个基本性质:
https://iswade.github.io/articles/partition/
哈希分区是最常见的数据分区方式,通过按照数据的key、或者用户指定的一个或者多个字段计算哈希,然后将计算后的哈希与计算节点进行映射,从而将不同哈希值的数据分布到不同节点上。
例如,有3条记录(key1, value1), (key2, value2), (key3, value3)
通过对键进行计算哈希(对 key 进行 md5哈希,然后取前两个字符作为哈希值),哈希桶个数255个,当前有两个分区,偶数哈希值放到分区0,奇数哈希值放到分区1,计算后的数据分布如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2CL3lSVq-1645429748408)(http://xwjpics.gumptlu.work/qinniu_uPic/partition_hash.svg)]
哈希分区需要仔细选取哈希函数,如果用户可以指定哈希字段时,则要考虑字段经过哈希之后散列性是否满足需求。如果选取的哈希函数散列性较好,则可以将数据大致均匀地分布到几个分区中。
哈希分区的一个优点是,保存的元数据很简单,只需要保存桶与分区的映射关系即可。但是缺点也很明显,可扩展性差,如果增加一个节点进行扩容,则需要对所有数据进行重新计算哈希,然后对数据进行重新分布,对于均匀分布的哈希函数而言,一般而言,在扩容时每个分区都需要扩容,通过成倍增加节点,然后通过调整映射关系,重新分布一半的数据到新分区。另一个缺点是,如果哈希函数选择不合理,则很容易出现数据倾斜,导致某个分区数据量很大。
位图,就是用一个或多个 bit 来标记某个元素对应的值,而键就是该元素。采用位作为单位来存储数据,可以大大节省存储空间。
位图通过使用位数组来表示某些元素是否存在。它可以用于快速查找,判重,排序等。不是很清楚?我先举个小例子。
假设我们要对 [0,7]
中的 5 个元素 (6, 4, 2, 1, 5) 进行排序,可以采用位图法。0~7 范围总共有 8 个数,只需要 8bit,即 1 个字节。首先将每个位都置 0:
0 0 0 0 0 0 0 0
然后遍历 5 个元素,首先遇到 6,那么将下标为 6 的位的 0 置为 1;接着遇到 4,把下标为 4 的位 的 0 置为 1:
0 0 0 0 1 0 1 0
依次遍历,结束后,位数组是这样的:
0 1 1 0 1 1 1 0
每个为 1 的位,它的下标都表示了一个数:
for i in range(8):
if bits[i] == 1:
print(i)
这样我们其实就已经实现了排序。(类似于计数排序,只不过计数排序是一个数组映射,数组的元素代表出现的次数)
对于整数相关的算法的求解,位图法是一种非常实用的算法。假设 int 整数占用 4B,即 32bit,那么我们可以表示的整数的个数为 232。
使用位图的特点:
https://zhuanlan.zhihu.com/p/43263751
类似于位图单不限于整数,本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
布隆过滤器数据结构
布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成**多个哈希值,**并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:
Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:
值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。
这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。
将10亿个数据分成1000份,每份100万个数据,找到每份数据中最大的那个数据,最后在剩下的1000个数据里面找出最大的数据。 从100万个数据遍历选择最大的数,此方法需要每次的内存空间为10^6*4=4MB,一共需要1000次这样的比较。(每100万个的topK可以用多协程并发计算)
对于top K类问题,通常比较好的方案是分治+hash+小顶堆:
先将数据集按照Hash分区方法分解成多个小数据集
- Hash(数据)%1000 先对数据Hash再取固定几位作为映射到不同的桶B0,B1,B2,…B999中
- 分解小问题,因为内存限制可能无法一次性将所有数据加载到内存
然后用小顶堆求出每个数据集中最大的K个数/前K个最大的数(内存允许可以并发多个一起)
- 每个小堆前K个最大的数一定包含最终的topK,因为原始数据中前K个大的数分到各个小数据集中一定还是在前K个大中
- 取前K个最大的数就是取大小为K的堆所有元素
最后在所有小堆的所有筛选出的数中,top K中求出最终的top K。
- 取堆顶即可
如果是top词频可以使用分治+ Trie树/hash +小顶堆:
先将数据集按照Hash分区方法分解成多个小数据集
然后使用Trie树或者Hash统计每个小数据集中的query词频(内存允许可以并发多个一起)
- Trie树节点记录字母以及当前路径表示字符串的出现次数
之后用小顶堆求出每个数据集中出频率最高的前K个数
- 遍历Trie树或Hash表
最后在所有top K中求出最终的top K。
时间复杂度:建堆时间复杂度是O(K), 算法的时间复杂度为O(NlogK)
例如int{1,4,5,6,9}, 输入的数是7,则结果为6,9
位图(数据大小范围已知),将10亿个数映射到位图中,然后将读取的数同样映射然后分别从左右两边开始寻找范围的数,最先找到的就是结果
位图法
构建两个bitmap,表达如下:
遍历 2.5 亿个整数,查看位图中对应的位,如果是 00,则变为 01,如果是 01 则变为 10,如果是 10 则保持不变。遍历结束后,查看位图,把对应位是 01 的整数输出即可。
那么这 2 32 2^{32} 232 个整数,总共所需内存为 2 32 2^{32} 232*2b=1GB。因此,当可用内存超过 1GB 时,可以采用位图法
给定 40 亿个不重复的没排过序的 unsigned int 型整数,然后再给定一个数,如何快速判断这个数是否在这 40 亿个整数当中?
位图,40 亿个不重复整数,我们用 40 亿个 bit 来表示,初始位均为 0,那么总共需要内存:4,000,000,000b≈512M。
我们读取这 40 亿个整数,将对应的 bit 设置为 1。接着读取要查询的数,查看相应位是否为 1,如果为 1 表示存在,如果为 0 表示不存在。
已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。
这道题本质还是求解数据重复的问题,对于这类问题,一般首先考虑位图法。
对于本题,8 位电话号码可以表示的号码个数为 108 个,即 1 亿个。我们每个号码用一个 bit 来表示,则总共需要 1 亿个 bit,内存占用约 100M。
思路如下:
申请一个位图数组,长度为 1 亿,初始化为 0。然后遍历所有电话号码,把号码对应的位图中的位置置为 1。遍历完成后,如果 bit 为 1,则表示这个电话号码在文件中存在,否则不存在。bit 值为 1 的数量即为 不同电话号码的个数。
整数数据 不同的个数,相同的个数,都可以考虑位图
中位数如果排序后就可以转换为:
如果没有内存限制则做法是:直接通过排序算法得到对应位置的数
如果有内存限制的做法:
双堆法:
分治法:
类似的题:两个 10G 大小包含 URL 数据的文件,最多使用 1G 内存,将这两个文件合并,并找到相同的 URL
对于A文件的每一条URL数据首先经过Hash(url)%1000,得到a0, a1, a2, …, a999个小数据集;同理对B文件也是,得到b0, b1, b2, …, b999个小数据集;
作用:
- 大数据集的Hash分区
- A,B文件中相同URL的数据经过上述Hash算法一定会映射到同一个桶中即 a i a_i ai与 b i b_i bi大部分URL是相同的,小部分可能因为Hash碰撞不同。
然后对于每对 a i a_i ai与 b i b_i bi,对 a i a_i ai的每条URL构建HashMap,然后判断 b i b_i bi中是否存在,存在的保留到结果 c i c_i ci
最后合并所有结果 c i c_i ci得到文件C
对于 TopK 问题,最常用的方法是使用堆排序。对本题而言,假设数组降序排列,可以采用以下方法:
(与合并k个链表思路相同)
位图,需要内存如下:1M不到
如果数据范围已知,则可以使用位图
如果数据范围未知,则可以使用topK(Hash分区=>小文件取前topK个数=>topK)
因为日志值的数字范围时有限的,首先预处理成一样的长度缺少的最前面补充0,然后用基数排序比较好,对于每一位可以用计数排序,从低位向高位来排序,时间复杂度为O(N*K)其中K为日志日期字符串大小