目录

  • 海量数据处理
  • 算法与数据结构基础
  • 海量数据处理方法归纳
  • 分而治之 / hash 映射 + hash 统计 + 堆 / 快速 / 归并排序
  • 多层桶结构
  • Bitmap / Bloom filter
  • Bitmap
  • Bloom filter
  • Trie树/数据库/倒排索引
  • Trie树
  • 数据库索引
  • 倒排索引(Inverted index)
  • 外排序
  • 分布式处理之Hadoop/Mapreduce
  • 参考链接



本文主要讲解海量数据处理方法的总结,由于字数较多,为了有更好的阅读体验,海量数据处理问题总结请移步海量数据处理问题总结

数据时代来临,数据量的爆炸式增长是最为显著的特征。当高性能硬件的普及还跟不上这样的数据大潮时,如何在有限的时空资源内处理海量数据成为了计算机科学以及数理统计等领域最大的挑战。

海量数据处理

海量数据处理,是基于海量数据上的存储、处理、操作。何谓海量,就是数据量太大,所以导致要么是无法在较短时间内迅速解决,要么是数据太大,导致无法一次性装入内存。

海量数据处理的困难用一句话概括,就是时间和空间资源不够。具体来说,

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

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

至于所谓的单机及集群问题,通俗点来讲,单机就是处理装载数据的机器有限(只要考虑 cpu,内存,硬盘的数据交互),而集群,机器有多个,适合分布式处理,并行计算(更多考虑节点和节点间的数据交互)。

处理海量数据问题的方法

  1. 分而治之 / hash映射 + hash统计 + 堆/快速/归并排序;
  2. 双层桶划分
  3. Bloom filter / Bitmap;
  4. Trie树 / 数据库 / 倒排索引;
  5. 外排序;
  6. 分布式处理之Hadoop / Mapreduce。

算法与数据结构基础

STL容器分为三种:

  1. 序列容器:vector、 list、deque、string
  2. 关联容器:set、multiset、map、mulmap、hash_set、hash_map、hash_multiset、hash_multimap
  3. 其他的杂项: stack、queue、valarray、bitset

关联式容器,关联式容器又分为set(集合)和map(映射表)两大类,以及这两大类的衍生体 multiset (多键集合)和 multimap (多键映射表),这些容器均以 RB-tree 完成。此外,还有第3类关联式容器,如 hashtable (散列表),以及以 hashtable 为底层机制完成的 hash_set (散列集合) / hash_map (散列映射表)、hash_multiset (散列多键集合)、hash_multimap (散列多键映射表)。也就是说,set/map、multiset、multimap都内含一个 RB-tree,而hash_set、hash_map、hash_multiset、hash_multimap都内含一个hashtable。
所谓关联式容器,类似关联式数据库,每笔数据或每个元素都有一个键值(key)和一个实值(value),即所谓的Key-Value(键-值对)。当元素被插入到关联式容器中时,容器内部结构(RB-tree/hashtable)便依照其键值大小,以某种特定规则将这个元素放置于适当位置。

包括在非关联式数据库中,比如,在 MongoDB 内,文档(document)是最基本的数据组织形式,每个文档也是以 Key-Value(键-值对)的方式组织起来。一个文档可以有多个Key-Value组合,每个 Value 可以是不同的类型,比如String、Integer、List等等。

注:详细了解STL容器可移步STL容器介绍

  • set / map / multiset / multimap
    set 同 map 一样,所有元素都会根据元素的键值自动被排序,因为 set / map两者的所有各种操作,都只是转而调用 RB-tree 的操作行为,不过,值得注意的是,两者都不允许两个元素有相同的键值
    不同的是:set 的元素不像 map 那样可以同时拥有实值(value)和键值(key),set元素的键值就是实值,实值就是键值,而 map 的所有元素都是 pair,同时拥有实值(value)和键值(key),pair的第一个元素被视为键值,第二个元素被视为实值。
    至于multiset/multimap,他们的特性及用法和 set / map完全相同,唯一的差别就在于它们允许键值重复,即所有的插入操作基于 RB-tree 的 insert_equal() 而非insert_unique()。
  • hash_set / hash_map / hash_multiset / hash_multimap
    hash_set / hash_map,两者的一切操作都是基于 hashtable 之上。不同的是,hash_set 同 set一样,同时拥有实值和键值,且实质就是键值,键值就是实值,而 hash_map 同 map 一样,每一个元素同时拥有一个实值(value)和一个键值(key),所以其使用方式,和上面的map基本相同。但由于 hash_set / hash_map 都是基于hashtable之上,所以不具备自动排序功能。为什么?因为 hashtable 没有自动排序功能。
    至于 hash_multiset / hash_multimap 的特性与上面的 multiset / multimap 完全相同,唯一的差别就是它们 hash_multiset / hash_multimap 的底层实现机制是hashtable(而multiset / multimap,上面说了,底层实现机制是 RB-tree),所以它们的元素都不会被自动排序,不过也都允许键值重复。

综上,什么样的结构决定其什么样的性质,因为 set / map / multiset / multimap 都是基于 RB-tree 之上,所以有自动排序功能,而 hash_set / hash_map / hash_multiset / hash_multimap 都是基于 hashtable 之上,所以不含有自动排序功能,至于加个前缀 multi_ 无非就是允许键值重复而已。:

海量数据处理方法归纳

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

  • 解决问题:海量数据不能一次性读入内存,而我们需要对海量数据进行的计数、排序等操作
  • 使用工具:hash函数(hash表);堆
  • 问题实例
  1. 给你 A,B 两个文件,各存放 50 亿条 URL,每条 URL 占用 64 字节,内存限制是 4G,让你找出 A,B 文件共同的 URL。如果是三个乃至 n 个文件呢?
  2. 海量日志数据,提取出某日访问百度次数最多的那个 IP。
  • 解题思路
  1. 分而治之 / hash映射:针对数据太大,内存受限,只能把大文件化成(取模映射)小文件
  2. hash_map 统计:当大文件转化了小文件,那么我们便可以采用常规的hash_map(key,value)来进行频率统计。
  3. 堆 / 快速排序:统计完了之后,便进行排序(可采取堆排序),得到次数最多的key。

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

海量数据处理架构 海量数据管理_算法


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

问题1 top-k筛选:海量数据存储于多个文件,任何一条数据都可能存在于任何一个文件当中,现需要筛选出现的次数最多的 k 条数据。
解决思路:

  1. 依次遍历这些文件,通过 hash 映射,将每个文件的每条数据映射到新构造的多个小文件中(设生成了n个小文件);
  2. 依次统计每个小文件中出现次数最多的 k 条数据,构成 hash 表,hash 表中每个键值对的形式为 dataItem: count;
  3. 利用堆排序,依次遍历这些hash表,在 n∗k 条数据中,找出 count 值最大的 k 个;

这里之所以使用堆排序,也是为了能尽可能地提高排序效率。就上例而言,堆排序的时间复杂度为 nklog(k)

问题2 对比查重:现有A和B两个大文件,每个文件都存储着海量数据,要求给出A,B中重复的数据。
解决思路:

  1. 遍历A中的所有数据,通过hash映射将他们分布存储在 n 个小文件中,记为 {a1,a2,…,an};
  2. 遍历B中的所有数据,通过hash映射将他们分布存储在 n 个小文件中,记为{b1,b2,…,bn};
  3. 根据hash函数性质可知,A 和 B 中的相同数据一定被映射到序号相同的小文件,所以我们依次比较{ai,bi}即可;

如果问题更进一步,要求返回重复次数最多的 k 条数据,则可以将对比小文件找到的数据存入hash表,键为数据,值为该数据出现的次数。再用大小为 k 的堆,排序找出即可。

多层桶结构

解决问题:海量数据求取第 k 大数;中位数;不重复或重复的数字
使用工具:hash 函数
问题实例

  1. 2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。、
  2. 5亿个int找它们的中位数。

多层桶结构其实和最开始我们用 hash 映射分割大文件的思路是一致的,都是一种“分而治之”的策略。只不过多层桶结构是对有些桶分割之后再分割,构成了一种层次化的结构,它主要应用于一次分割的结果依然不能解决内存限制的情况。

问题3 求取海量整数的中位数
解决思路:

  1. 依次遍历整数,按照其大小将他们分拣到 n 个桶中。但是现在出现了一种不好的情况,就是可能有的桶数据量很小,有的则数据量很大,大到内存放不下了;
  2. 对于那些太大的桶,我们就再分割成更小的桶,当然分割的准则还是依据数据大小,这样,其实是构成了一种类似于不平衡的多叉树的结构;
  3. 根据多叉树第一层的数量统计结果,我们可以知道中位数在哪个节点中,如果该节点还有孩子,就判断在其哪个孩子节点中,直到找到叶子节点,最后找出目标。

Bitmap / Bloom filter

Bitmap

Bitmap 就是用一个 bit 位来标记某个元素对应的 Value, 而 Key 即是该元素。由于采用了 Bit 为单位来表示某个元素是否存在,因此在存储空间方面,可以大大节省。

问题实例

  1. 已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
  2. 2.5 亿个整数中找出不重复的整数的个数,内存空间不足以容纳这 2.5 亿个整数。

Bitmap排序方法

  1. 将所有的位都置为0,从而将集合初始化为空。
  2. 通过读入文件中的每个整数来建立集合,将每个对应的位都置为1。
  3. 检验每一位,如果该位为1,就输出对应的整数。

例如:我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复);那么我们就可以采用Bitmap的方法来达到排序的目的。要表示8个数,我们就只需要8个 Bit(1Bytes);将对应的第四位置为1,最终处理结果如下:

海量数据处理架构 海量数据管理_字符串_02

我们只想知道某个元素出现过没有。如果为每个所有可能的值分配1个 bit;但对于海量的、取值分布很均匀的集合进行去重,Bitmap 极大地压缩了所需要的内存空间。与此同时,还额外地完成了对原始数组的排序工作。缺点是,Bitmap 对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

Bloom filter

解决问题:数据字典的构建;判定目标数据是否存在于一个海量数据集;集合求交集
使用工具:Bloom Filter;hash函数
基本原理:当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检索元素一定不在;如果都是1,则被检索元素很可能在。

以存在性判定为例,Bloom Filter 通过对目标数据的映射,能够以 O(k) 的时间复杂度判定目标数据的存在性,其中 k 为使用的 hash 函数个数。这样就能大大缩减遍历查找所需的时间。

注:有关Bloom Filter树的详细讲解可以参考博客布隆过滤器(Bloom Filter)的原理和实现,建议去了解一下。宁可误报,不可错报

问题4 集合求交:与上面的问题2类似,只不过现在不是A和B两个大文件,而是A, B, C, D….多个大文件。
解决思路:

  1. 依次遍历每个大文件中的每条数据,遍历每条数据时,都将它插入Bloom Filter;
  2. 如果已经存在,则在另外的集合(记为 S)中记录下来;
  3. 如果不存在,则插入Bloom Filter;
  4. 最后,得到的 S 即为所有这些大文件中元素的交集

问题5 unsigned int型整数存在性判定:判定一个unsigned int型整数是否在一个大的unsigned int型整数数据集中。
解决思路:

  1. 假设 unsigned int 型整数数据集长度为 N,则申请一个大小为 512M 的数组作为 Bloom Filter;
  2. 遍历数据集,按照遍历到的整数(记为 a)将 Bloom Filter 的第 a 位变成1;
  3. 检查 Bloom Filter 中,目标数据所代表的位是 0 还是 1;

问题5用的其实是一种简化了的Bloom Filter,不再采取hash映射的方式,而是直接根据整数的大小确定要改变的位数,这在某些特殊情况下(比如数据种类不多时)非常有效。

Trie树/数据库/倒排索引

Trie树

解决问题:当需要处理的是海量字符串数据时,有时Trie树会比直接上面说的hash映射的策略更高效。
适用范围:数据量大,重复多,但是数据种类小可以放入内存
使用工具:Trie 树;堆
扩展:压缩实现。
问题实例

  1. 有 10 个文件,每个文件 1G,每个文件的每一行都存放的是用户的 query,每个文件的 query 都可能重复。要你按照 query 的频度排序。
  2. 1000 万字符串,其中有些是相同的(重复),需要把重复的全部去掉,保留没有重复的字符串。请问怎么设计和实现?
  3. 寻找热门查询:查询串的重复度比较高,虽然总数是 1 千万,但如果除去重复后,不超过 3百万个,每个不超过 255 字节。
    结构
    Trie树是如图所示的一棵多叉树。其中存储的字符串集合为:
    {“a”,“aa”,“ab”,“ac”,“aab”,“aac”,“bc”,“bd”,“bca”,“bcc”}

    从上图我们可以看出,Trie树有如下3点特征:
  • 根节点不代表(包含)字符,除根节点外每一个节点都只代表(包含)一个字符。
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  • 每个节点的所有子节点包含的字符都不相同。

其实,一棵完整的 Trie 树应该每个非叶节点都拥有 26 个指针,正好对应着英文的 26 个字母,这样整棵树的空间成本为26L,L 为最长字符串的长度。但是为了节省空间,我们可以根据字符串集本身为每个非叶节点,“量身定做”子节点。以上面的图为例,以 ”a” 开头的字符串中,第二个字符只有 ”a, b, c” 3种可能,我们当然没有必要为节点 u1 生成 26 个子节点了,3 个就够了。

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

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

应用

  1. 节约字符串的存储空间
    假设现在我们需要对海量字符串构建字典。所谓字典就是一个集合,这个集合包含了所有不重复的字符串,字典在对文本数据做信息检索系统时的作用我想毋庸赘述了。那么现在就出现了一个问题,那就是字典对存储空间的消耗过大。而当这些字符串中存在大量的串拥有重复的前缀时,这种消耗就显得过于浪费了。比如:“ababc”, “ababd”, “ababrf”, “abab…”,这些字符串几乎都拥有公共前缀 ”abab”。 我们直接的想法是,能不能通过一种存储结构节约存储成本,使得所有拥有重复前缀的串对于公共前缀只存储一遍。这种存储的应用场景如果是对DNA序列的存储,那么出现重复前缀的可能性更大,空间需求也就更为强烈。
  2. 字符串检索
    检索一个字符串是否属于某个词典时,我们当前一般有两种思路:
    a. 线性遍历词典,计算复杂度 O(n),n 为词典长度;
    b. 利用hash表,预先处理字符串集合。这样再搜索运算时,计算复杂度 O(1)。但是 hash 计算可能存在碰撞问题,一般的解决办法比如对某个 hash 值所代表的字符串实施二次检索,则计算时间也会上来。而且,hash 虽说是一种高效算法,其计算效率比直接字符匹配还是要略高的。
  3. 字符串公共前缀问题
    两个非常典型的例子:
    a. 求取已知的 n 个字符串的最长公共前缀,朴素方法的时间复杂度为 O(nt),t 为最长公共前缀的长度;
    b. 给定字符串 a,求取 a 在某 n 个字符串中和哪些串拥有公共前缀

对于问题 b,除了朴素的比较法之外,我们还可以采取对每个字符串的所有前缀计算 hash 值的方法,这样一来,计算所有前缀 hash 值复杂度 O(n∗len),len 为字符串的平均长度,查询的复杂度为 O(n)。虽然降低了查询复杂度,但是计算hash值显然费时费力。

问题6 数据去重:一个超大文件(不能直接读入内存),里面包含海量字符串数据,但字符串数据种类有限(可见含有大量重复),现需要对字符串去重。并统计去重后每个字符串出现的次数
解决思路:

  • 将大文件分割成多个小文件,依次遍历每个小文件,读取其中存储的每一个字符串,构建Trie树,并在每一个终止节点记录该节点代表的字符串(即从根节点到该节点的字符组成的字符串)当前出现的次数,解决问题的时间复杂度为 O(N∗len),其中 N 为字符串总数,len 为字符串的平均长度;
  • 如果问题更进一步,需要排序,那就再建一个堆,读取 Trie 树,将字符串依次插入堆,时间复杂度为 O(N′∗len∗log⁡N′),其中 N′ 是去重后字符串的数量。

总结一下,Trie树对于海量字符串数据,在数据种类有限(构建的Trie树可以完全读入内存)时,能够使我们轻松的进行存储,查找,计数等工作。

数据库索引

适用范围:大数据量的增删改查
基本原理及要点:利用数据的设计实现方法,对海量数据的增删改查进行处理。

倒排索引(Inverted index)

适用范围:搜索引擎,关键字查询
基本原理及要点:一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。

外排序

适用范围:大数据的排序,去重
基本原理及要点:外排序的归并方法,置换选择败者树原理,最优归并树
问题实例:有一个 1G 大小的一个文件,里面每一行是一个词,词的大小不超过 16 个字节,内存限制大小是 1M。返回频数最高的 100 个词。

两个独立阶段

  1. 首先按内存大小,将外存上含n个记录的文件分成若干长度L的子文件或段。依次读入内存并利用有效的内部排序对他们进行排序,并将排序后得到的有序字文件重新写入外存,通常称这些子文件为归并段。
  2. 对这些归并段进行逐趟归并,使归并段逐渐由小到大,直至得到整个有序文件为之。

外排序的优化方法:置换选择败者树原理,最优归并树。

提高外部排序需要考虑以下问题

  1. 如何减少排序所需的归并趟数。
  2. 如果高效利用程序缓冲区,使得输入、输出和CPU运行尽可能地重叠。
  3. 如何生成初始归并段(Segment)和如何对归并段进行归并。

实例

问题7 要对外存中 4500 个记录进行归并,而内存大小只能容纳750个记录。

  1. 每次读取 750 个记录进行排序,这样可以分六次读取,进行排序,可以得到六个有序的归并段,每个归并段的大小是 750 个记录,记住,这些归并段已经全部写到临时缓冲区(由一个可用的磁盘充当)内了,这是第一步的排序结果。
  2. 将内存空间划分为三份,每份大小 250 个记录,其中两个用作输入缓冲区,另外一个用作输出缓冲区。首先对 Segment_1 和 Segment_2 进行归并,先从每个归并段中读取 250 个记录到输入缓冲区,对其归并,归并结果放到输出缓冲区,当输出缓冲区满后,将其写道临时缓冲区内,如果某个输入缓冲区空了,则从相应的归并段中再读取 250 个记录进行继续归并,反复以上步骤,直至 Segment_1 和Segment_2 全都排好序,形成一个大小为 1500 的记录,然后对 Segment_3 和 Segment_4、Segment_5 和 Segment_6 进行同样的操作。
  3. 对归并好的大小为 1500 的记录进行如同步骤1一样的操作,进行继续排序,直至最后形成大小为 4500 的归并段,至此,排序结束。

分布式处理之Hadoop/Mapreduce

适用范围:数据量大,但是数据种类小可以放入内存
基本原理及要点:将数据交给不同的机器去处理,数据划分,结果归约。
问题实例

  1. The canonical example application of MapReduce is a process to count the appearances of
    each different word in a set of documents:
  2. 海量数据分布在100台电脑中,想个办法高效统计出这批数据的TOP10。
  3. 一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到N^2个数的中数(median)?

MapReduce是一种计算模型,分为”Map”和”Reduce”两个阶段:

  • Map:将大量数据分割后(或者将困难任务分解后)“各个击破”;
  • Reduce:将“各个击破”的结果按照一定的规律进行合并操作;

这样做的好处是可以在任务被分解后,可以通过大量机器进行并行计算,从而突破时间或者空间的限制。时间上显然更快,空间上,单个机器只需要处理空间允许的数据量即可。举个简单的例子就是归并排序,我们先将数据集分割成小数据集,使用多个机器排序每个分割后的小数据集(Map),再将处理好的归并段依次归并(Reduce)。