海量数据处理基本方法总结

本文总结了海量数据处理的挑战与常用方法,包括哈希映射、分文件处理、Bloom Filter、Trie树、外排序和多层桶结构。通过这些策略解决时间和空间资源限制,例如哈希映射通过分而治之策略将数据分布到多个小文件,Bloom Filter用于高效判断数据是否存在,Trie树节省存储空间和加速字符串检索,外排序用于海量数据排序,多层桶结构处理内存限制下的大数据问题。

一. 海量数据处理的困难

海量数据处理的困难用一句话概括就是:时空(时间,空间)资源不够。

  • 时间受限:无法在有限的时间内,完成针对海量数据的某项特定工作的处理;
  • 空间受限:无法将海量数据一次性读入内存;

对于时间受限的问题,我们一般的解决办法是高效的算法配合恰当的数据结构,比如哈希表,堆,二叉树等;而对于空间受限的问题,一般的解决办法就是 “大而化小,分而治之” 的策略,既然一次行不通,那就一部分一部分的读取,每读入一部分可以生成一个小文件,小文件是可以直接读放入内存中去的,然后就这样分割大数据之后,再依次处理各个的小文件的数据,最后归并即可。

二.常用方法

1.哈希映射
  • 解决问题:海量数据不能一次性读入内存,而我们需要对海量数据进行相应的操作(计数、排序等操作)
  • 使用工具:hash函数(hash表);堆

这种方法是典型的== “分而治之”== 的策略,也是解决空间限制最常用的方法,基本思路可以用下图表示。先借助哈希算法,计算每一条数据的 hash 值,按照 hash 值将海量数据分布存储到多个桶中(所谓桶,一般可以用小文件实现)。根据 hash 函数的唯一性,相同的数据一定在同一个桶中。如此,我们再依次处理这些小文件,最后做合并运算即可。
在这里插入图片描述
注:一般用 hash 函数将数据映射到桶的方法是:

  • bucket_ID = H(mi) % n

其中 H(mi) 为第 i 条数据,bucket_ID 为桶标号,n 为要设置的桶的数量。关于桶数量(即小文件数量)设置的基本的原则是:每个小文件的大小比内存限制要小。
比如处理 1G 的大文件,内存限制为 1M,那就可以把大文件分成 2000 个小文件(甚至更多),这样每个小文件的大小约 500K (甚至更小),我们就可以轻松读入内存进行处理了。


2.分文件+哈希统计+内存处理(堆/快速/归并排序)

对于固定大小的海量数据,通常可以采用 分文件+哈希统计+内存处理(堆/快速/归并排序) 的方法。

  • 对于字符串数据,可以对字符串进行哈希(字符串.hashCode() ),哈希值%n ( n为分成的小文件的数量 ),这样来说同一个字符串必然分配到同一个小文件当中,然后如果哈希均匀的话(每个小文件的数量差不多一样多,不会出现一个小文件的数量特别多,另一个小文件的数量特别少的情况),就能够保证每个小文件都可以放到内存当中去,在内存当中采用通用方法进行处理,获得每个小文件的结果,然后挨个写到磁盘中,最后汇总或者直接在内存中汇总即可。
  • 对于数字,可以直接 num%n(n为分的小文件的数量),方法和对字符串数据处理方法一样。

3.Bloom filter(布隆过滤器)

  • 解决问题:数据字典的构建;判定目标数据是否存在于一个海量数据集;集合求交
  • 使用工具:Bloom Filter;hash函数
  • 以存在性判定为例,Bloom Filter通过对目标数据的映射,能够以 O(k)的 时间复杂度判定目标数据的存在性,其中 k 为使用的 hash 函数的个数,这样就能大大缩减遍历查找所需的时间。

布隆过滤器:

  • 选 k 个哈希函数,记为 {h1,h2,…,hk}
  • 假设现在有 n 个元素需要被映射到 bit 数组中,bit 数组的长度是 m . 初始时,将 m 大小的 bit 数组的每个位置的元素都置为 0 。
  • 现在,把这个 n 个元素依次用第 1 步选取的 k 个哈希函数映射到 bit 数组的位置中去,bit 数组被映射到的位置的元素变为1。显然,一个元素能被映射到 k 个位置上。过程如图所示,现在把元素集合 {x,y,z} 通过 3 个哈希函数映射到一个二进制数组中。
  • 最后,需要检查一个元素是否在已有的集合中时,同样用这 k 个哈希函数把要判断的元素映射到 bit 数组上去,只要 bit 数组被映射到的位置中有一个位不是 1 ,那一定说明了这个元素不在已有的集合内。

如图:W 元素就不在集合中
在这里插入图片描述
我们为什么用Trie树?

  1. 节约字符串的存储空间
    假设现在我们需要对海量字符串构建字典。所谓字典就是一个集合,这个集合包含了所有不重复的字符串,那么现在就出现了一个问题,那就是字典对存储空间的消耗过大,而当这些字符串中存在大量的串拥有重复的前缀时,这种消耗就显得过于浪费了。比如:“ababc”,“ababd”,“ababrf”,“abab…”,…“ababc”,“ababd”,“ababrf”,“abab…”,…,这些字符串几乎都拥有公共前缀 ”abab”。 我们直接的想法是,能不能通过一种存储结构节约存储成本,使得所有拥有重复前缀的串只存储一遍。这种存储的应用场景如果是对DNA序列的存储,那么出现重复前缀的可能性更大,空间需求也就更为强烈。

  2. 字符串检索
    检索一个字符串是否属于某个词典时,我们当前一般有两种思路:
    线性遍历词典,计算复杂度 O(n),n 为词典长度;利用 hash 表,预先处理字符串集合。这样再搜索运算时,计算复杂度 O(1)。但是 hash 计算可能存在碰撞问题,所以,能不能设计一种高效的数据结构帮助解决字符串检索的问题?


4.Trie树(前缀树)

  • 实现方式:节点孩子的表示方式
  • 扩展:压缩实现。
  • 适用范围:在字典的存储,字符串的查找,求取海量字符串的公共前缀,以及字符串统计等方面发挥着重要的作用。
  • 适用范围:Trie 树是一种非常强大的处理海量字符串数据的工具。尤其是大量的字符串数据中存在前缀时,Trie 树特别好用。用于存储时,Trie 树因为不重复存储公共前缀,节省了大量的存储空间;用于字符串的查找时,Trie 树依靠其特殊的性质,实现了在任意数据量的字符串集合中都能以 O(len) 的时间复杂度完成查找(len 为要检索的字符串长度);在字符串统计中,Trie 树能够快速记录每个字符串出现的次数。

Trie树是如图所示的一棵多叉树。其中存储的字符串集合为:
{“a”,“aa”,“ab”,“ac”,“aab”,“aac”,“bc”,“bd”,“bca”,“bcc”}

在这里插入图片描述
从上图我们可以看出,Trie 树有如下3点特征:

  • 根节点不代表字符,除根节点外每一个节点都只代表一个字符串(一般的解释是:除根节点外所有节点只 “包含” 一个字符串。
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  • 每个节点的所有子节点包含的字符都不相同。
  • 其实,一棵完整的 Trie 树应该每个非叶节点都拥有 26 个子节点,正好对应着英文的 26 个字母,这样整棵树的空间成本为 26^l ,l 为最长字符串的长度。但是为了节省空间,我们可以根据字符串集本身为每个非叶节点,“量身定做”子节点。以上面的图为例,以 ”a” 开头的字符串中,第二个字符只有 ”a, b, c” 3 种可能,因此我们当然没有必要为节点 u1 生成 26 个子节点,所以 3 个子节点就够了。

除此之外,由于有些字符串就是集合中其他字符串的前缀,为了能够分辨清楚集合中到底有哪些字符串,我们还需要为每个节点赋予一个判断终止与否的 boolean 值,记为 end 。比如上图,由于同时存在字符{“a”,“ab”,“ac”,“aa”,“aab”,“aac”} 我们就令节点 u1,u2 的 end 值为 True,表示从根节点到 u1,u2 的路径上的字符按顺序可以构成集合中一个完整的字符串(如”a”, “aa”)图中,我们将end == True的节点标红。


5.外排序

  • 解决问题:海量数据的排序
  • 实用工具:归并算法
  • 针对内容:外部排序是针对海量数据无法一次性放入内存中而产生的一种方法。
  • 基本算法是:将海量数据拆分成多个小文件,每个小文件可以一次性放入内存当中,然后对于每一个小文件进行排序后(这里采用任何排序算法都可以),再采用多路归并排序即可。多路归并也就是构造一个大小为小文件数量的堆(升序构造小堆,降序构造大堆)。

6.多层桶结构

  • 解决问题:海量数据求取第 k 大的数
  • 使用工具:hash 函数
  • 多层桶结构其实和用 hash 映射,分割大文件的思路是一致的,都是一种“分而治之” 的策略。只不过多层桶结构是对有些桶分割之后再分割,构成了一种层次化的结构,它主要应用于一次分割的结果依然不能解决内存限制的情况。

三.常见问题

1.分而治之+hash映射+快速/归并/堆排序

给定 a、b 两个文件,各存放 50 亿个 url ,每个 url 各占 64 个字节,内存限制是 4G,让你找出 a、b 文件共同的 url?

分析:50亿*64字节 = 320G 大小空间。
算法思想:hash 分解+ 分而治之 + 归并

  • 遍历文件 a,对每个 url 根据某种 hash 规则求取 hash(url)/1024,然后根据所取得的值将 url 分别存储到1024个小文件( a0- a1023)中,这样每个小文件的大约为 300M 。如果 hash 结果很集中使得某个文件 ai 过大,可以在对 ai 进行二级 hash( ai0~ ai1024)。
  • 这样 url 就被 hash 到 1024 个不同级别的目录中。然后可以分别比较文件,a0 VS b0……a1023 VS b1023,求每对小文件中相同的 url 时,可以把其中一个小文件的 url 存储到 hash_map 中。然后遍历另一个小文件的每个 url ,看其是否存在于刚才构建的 hash_map 中,如果是,那么就是共同的url ,存到文件里面就可以了,如果不是则删除。
  • 把 1024 个级别目录下相同的 url 合并起来(存储到 hash_map 中)。
  • 最后 hash_map 中的就是最终结果。

有 10 个文件,每个文件 1G,每个文件的每一行存放的都是用户的 query ,每个文件的 query 都可能重复,要求你按照 query 的频度排序。

解决思想1:hash分解+ 分而治之 +归并

  • 顺序读取 10 个文件 a0-a9 ,按照 hash(query)%10 的结果将 query 写入到另外 10 个文件(记为 b0~b9)中。这样新生成的文件每个的大小也约为1G(假设 hash 函数是随机的)。
  • 找一台内存 2G 左右的机器,依次对用 hash_map(query, query_count) 来统计每个 query 出现的次数。利用 快速/堆/归并排序 按照出现次数进行排序。将排序好的 query 和对应的 query_cout 输出到文件中。这样得到了10个排好序的文件 c0~c9 。
  • 对这 10 个文件 c0-c9 进行 归并排序(内排序与外排序相结合)。每次取c0~c9 文件的 m 个数据放到内存中,进行 10m 个数据的归并,即使把归并好的数据存到 d 结果文件中。如果 ci 对应的 m 个数据全归并完了,再从 ci余下的数据中取 m 个数据重新加载到内存中,直到所有 ci 文件的所有数据全部归并完成。

解决思想2: Trie树

  • 如果 query 的总量是有限的,只是重复的次数比较多而已,可能对于所有的 query,一次性就可以加入到内存了。在这种假设前提下,我们就可以采用 trie树/hash_map 等直接来统计每个 query 出现的次数,然后按出现次数做 快速/堆/归并排序 就可以了。

有一个 1G 大小的一个文件,里面每一行是一个词,词的大小不超过 16 字节,内存限制大小是 1M,返回频数最高的100个词。
类似问题:怎么在海量数据中找出重复次数最多的一个?

解决思想: hash分解+ 分而治之+归并

  • 顺序读文件,对于每个词 x,按照 hash(x)/(1024*4) 存到 4096个小文件中。这样每个文件大概是 250k 左右。如果哈希冲突严重,其中有的文件大小超过了 1M ,该文件还可以按照 hash 继续往下分,直到分解得到的小文件的大小都不超过 1M。
  • 对每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map 等),并存入4096个文件。
  • 下一步就是把这4096个文件进行归并的过程了。(类似与归并排序)
  • 然后去前100个此就行。

在 2.5 亿个整数中找出不重复的整数,内存不足以容纳这 2.5 亿个整数。

解决思路 : hash 分解+ 分而治之 + 归并

  • 2.5 亿个 int 数据 hash 到 1024 个小文件中 (a0-a1023),如果某个小文件大小还大于内存,则对这个小文件进行多级 hash。每个小文件读进内存,找出只出现一次的数据,输出到 b0~b1023 文件中。
  • 最后数据合并即可。

上千万或上亿数据(有重复),统计其中出现次数最多的前 N 个数据。

解决思路: 红黑树 + 堆排序

  • 如果是上千万或上亿的 int 数据,现在的机器 4G 内存可以能存下,所以考虑采用 hash_map/搜索二叉树/红黑树 等来进行统计重复次数。
  • 然后取出前 N 个出现次数最多的数据。

此处附上原文连接:https://blog.csdn.net/hong2511/article/details/80842704

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值