海量数据处理

  针对海量数据的处理,可以使用的方法非常多,常见的方法有hash法、Bit-map法、Bloom filter法、数据库优化法、倒排索引法、外排序法、Trie树、堆、双层桶法以及MapReduce法。

1、hash法

hash法也成为散列法,它是一种映射关系,即给定一个元素,关键字是key,按照一个确定的散列函数计算出hash(key),把hash(key)作为关键字key对应的元素的存储地址,再进行数据元素的插入和检索操作。

  散列表是具有固定大小的数组,表长应该是质数,散列函数是用于关键字和存储地址之间的一种映射关系,但是,不能保证每个元素的关键字与函数值是一一对应的,因为可能会冲突(多个关键字对应同一个存储地址)。

  常用的散列函数的构造方法有:

  (1)直接寻址法

  取关键字或关键字的某个线性函数值为散列地址,即h(key) = key或h(key)=a*key+b,其中a和b都是整型常数,这种散列函数叫做自身函数。这种函数的时间复杂度是O(1),但是空间复杂度是O(n),其中n指的是关键字的个数。

  直接寻址法不会导致哈希冲突,但是没有压缩,所以在关键值集合较大的时候,使用这种hash函数不能实现地址编码的散列。

  (2)取模法

  选择一个合适的正整数p,令hash(key)=key mod p,p如果选择的是比较大的素数,则效果比较好,一般p取的是散列表的长度。

  (3)数字分析法

  设关键字是d位的以r为基的数,且共有n个关键字,则关键字的每个位可能有r个不同字符出现,但这r个字符出现的频率不固定,可能在某些位上是俊宇的,即每个字符出现的次数接近于r/n,而在另外的一些位上分布不均匀。因此可以选取其中分布比较均匀的那些位,重新组合为新的数,用其作为散列地址。

  这种方法比较简洁,但是需要预知每个关键字的情况,这样就限制了使用。

  (4)折叠法 

  将关键字分成位数为t的几个部分(最后一部分的位数可能小于t),然后把各部分按位对其进行相加,将所得的和舍弃进位,留下t位作为散列地址。当关键字位数很多,而且关键字中每位上数字分布比较均匀时,采用折叠法比较合适。

  (5)平方取中法

  这是一种常见的方法,将关键字进行平方运算,然后从结果的中间取出若干位(位数与散列地址的位数相同),将其作为散列地址。

  (6)除留取余法

  这是一种比较常见的散列方法,其主要原理是取关键字除以某个数p(p不大于散列表的长度)的余数作为散列地址,即:

 hash(key) = key%p

     使用除留取余法时,选取合适的p值很重要,一般p<=TableSize,p一般取质数,也可以是不包含小于20质因子的合数。

  (7)随机数法

  选择一个随机函数,然后用关键字key的随机函数值作为散列地址,即

  hash(key) = random(key)

   其中,random()是随机函数。当关键字的长度不等的时候,采用这种方式比较合适。

  无论采用什么样的方法,冲突都是不可能避免的,所以解决冲突是一个必不可少的过程。解决冲突的主要途径是当一个关键字映射到散列表中的某一个地址,且该地址上已有关键字的时候,再为该关键字寻找新的存储地址。

  常用的解决冲突的方法有以下几种:

1.开放定址法

  开放定址法的基本思想是当发生地址冲突的时候,在散列表中再按照某种方法继续探测其他的存储地址,直到找到空闲的地址为止。这个过程可以描述为:

  Hi = (H(key) + di) mod m(i = 1,2,...,k (k <= m-1) )

  其中:

    H(key):关键字key的直接散列地址;

    m:散列表的长度;

    di:每次再探测时的地址增量。

  采用这种方法时,首先计算出关键字的直接散列地址,即H(key),若该直接散列地址上已经有其他关键字,则继续查看地址为H(key) + di 的存储地址,判断是否为空。如此反复,知道找到空闲的存储地址为止,然后将关键字key存放在该地址。

  增量di有不同的取法,常用的有以下3种:

  (1)di = 1,2,3,...,m-1,称为线性探测再散列;

  (2)di = 1*1, -1*1, 2*2, -2*2,...,k*k, -k*k,称为二次探测再散列;

  (3)di = 伪随机数,称为伪随机再散列。

2.链地址法(拉链法)

若散列表空间为[0,m-1],则设置一个由m个指针组成的一维数组CH[m],然后在寻找关键字散列地址的过程中,所有散列地址为i的数据元素都插入到头指针为CH[i]的链表中。

  这种方法适合于冲突比较严重的情况。

  对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法:

拉链法的优势与缺点

与开放定址法相比,拉链法有如下几个优点:

  1. 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  2. 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  3. 开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
  4. 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

拉链法的缺点:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

  (3)再散列法(再哈希法)

  当发生冲突的时候,使用第二个、第三个散列函数计算地址,直到没有冲突为止,但这种方法可能导致计算时间的大幅增加。

  (4)建立一个公共溢出区

  假设散列函数的值域为[0,m - 1],则设向量Hashtable [0...m-1]为基本表,另外设立存储空间向量OverTable[0...v]用以存储发生冲突的记录。

  hash主要用来进行“快速存取”,在O(1)的时间复杂度里就可以查找到目标元素,或者判断其是否存在。hash数据结构中的数据对外是杂乱无章的,因此其具体的存储位置以及各个存储元素位置之间的相互关系是无法得知的,但是却可以在常数时间里判断元素位置及存在与否。

2、Bit-map法

位图法的基本原理是使用位数组来表示某些元素是否存在,如从8位电话中查找重复号码。

  为突发的结果是生成一个N位长的串,每位上以“0”或“1”表示需要排序的组合(简称“集合”)中的数,例如集合为{2,7,4,9,1,10},则生成一个10位的串,将会在第2、7、4、9、1、10位置设置为“1”,其余位置为“0”,当把串中所有位都置完后,排序也自动完成了(因为字符串的下标是有序的)。上面的数据排序后的结果为1101001011。

  如果要排序0-15以内的元素序列{5,8,1,12,6,2},那么首先开辟了2Byte的空间,也就是16位,分别对应0-15这16个数,并将这16个位置设置为“0”。遍历序列,在出现的数字的对应位置上置为“1”,也就是将每个元素对应到了位图的相应位置。再遍历这16位,就完成了对元素的排序。

  使用位图法存储数据【5,1,7,15,0,4,6,10】,如下图所示:

  位图法排序的时间复杂度是O(n),比一般的排序快,但它是以时间换空间(需要一个N位的串)的,而且有一些限制,即数据状态不是很多,例如排序前集合大小最好已知,而且集合中元素的最大重复次数必须已知,最好数据比较集中。

  常常会遇到判断集合中是否存在重复的问题,数据量比较小的时候,对时间复杂度要求不高,担当集合中数据量比较大的时候,则希望能够少进行几次扫描,此时如果还采用双重循环的话,效率很低,此时使用位图法很合适,首先找到最大元素,然后按照集合中最大元素max创建一个长度为max+1的新数组,接着再次扫描原数组,每次遇到一个元素,就将新数组中下标为元素值的位置1,例如,如果遇到元素5,就将新数组中第6个位置置为1,当再次遇到5的时候,发现已经是1,所以重复。该算法的运算次数最坏的情况为2N,但如果知道最大元素,速度可以提升1倍。

3.Bloom Filter法

如需要判断一个元素是不是在一个集合中,我们通常做法是把所有元素保存下来,然后通过比较知道它是不是在集合内,链表、树都是基于这种思路,当集合内元素个数的变大,我们需要的空间和时间都线性变大,检索速度也越来越慢。 Bloom filter 采用的是哈希函数的方法,将一个元素映射到一个 m 长度的阵列上的一个点,当这个点是 1 时,那么这个元素在集合内,反之则不在集合内。这个方法的缺点就是当检测的元素很多的时候可能有冲突,解决方法就是使用 k 个哈希 函数对应 k 个点,如果所有点都是 1 的话,那么元素在集合内,如果有 0 的话,元素则不在集合内。

核心思想是:位数组和hash函数的联合使用。

(1)位数组:

        假设Bloom Filter使用一个m比特的数组来保存信息,初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0,即BF整个数组的元素都设置为0。

(2)添加元素,k个独立hash函数

       为了表达S={x1, x2,…,xn}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。

         当我们往Bloom Filter中增加任意一个元素x时候,我们使用k个哈希函数得到k个哈希值,然后将数组中对应的比特位设置为1。即第i个哈希函数映射的位置hashi(x)就会被置为1(1≤i≤k)。

 注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位,即第二个“1“处)。   

(3)判断元素是否存在集合

    在判断y是否属于这个集合时,我们只需要对y使用k个哈希函数得到k个哈希值,如果所有hashi(y)的位置都是1(1≤i≤k),即k个位置都被设置为1了,那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。下图中y1就不是集合中的元素(因为y1有一处指向了“0”位)。y2或者属于这个集合,或者刚好是一个false positive。

Bloom Filter的缺点:

       1)Bloom Filter无法从Bloom Filter集合中删除一个元素。因为该元素对应的位会牵动到其他的元素。所以一个简单的改进就是 counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。 此外,Bloom Filter的hash函数选择会影响算法的效果。

       2)还有一个比较重要的问题,如何根据输入元素个数n,确定位数组m的大小及hash函数个数,即hash函数选择会影响算法的效果。当hash函数个数k=(ln2)*(m/n)时错误率最小。在错误率不大于E的情况 下,m至少要等于n*lg(1/E) 才能表示任意n个元素的集合。但m还应该更大些,因为还要保证bit数组里至少一半为0,则m应 该>=nlg(1/E)*lge ,大概就是nlg(1/E)1.44倍(lg表示以2为底的对数)。 

举个例子我们假设错误率为0.01,则此时m应大概是n的13倍。这样k大概是8个。 

4.数据库优化法

这种方法不细致说,因为不是直接的算法,而是通过优化数据库(优化数据库其实也是用的算法)的方式。

5.倒排索引法

6.外排序法

当待排序的对象数目特别多的时候,在内存中不能被一次性处理,必须把它们以文件形式存放在外存中,排序的时候再把它们一部分一部分的调入内存进行管理,这种方式就是外排序法。

7.Trie树

Trie树又被称为字典树或者键树,它是一种用于快速字符串检索的多叉树结构,其原理是利用字符串的公共前缀来减少时空开销,即以空间换时间,从而达到提高程序效率的目的。Trie树的典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎用于文本词频统计。优点是可以最大限度的减少无畏的字符串比较,查询效率比散列表高。

  Trie树一般具有3个基本特性:

  (1)根节点不包含字符,除根节点之外的每一个节点都只包含一个字符;

  (2)从根节点到某一节点,路径上所经过的字符连接起来,为该节点对应的字符串;

  (3)每个节点的所有子节点包含的字符都不同。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小白的技术客栈

Python基础语法-流程控制

今天讲解Python的流程控制,流程控制也比较简单,小白不想整的很复杂,以免让大家看了有一种望“文”生怯的想法。 程序控制结构 通常的程序设计语言有三种控制结构...

31660
来自专栏mathor

LeetCode55. 跳跃游戏

 首先创建一个index数组,存储当前位置最大可达的数组下标,就以样例1来举例,输入是[2,3,1,1,4],那么对应的这个index数组就是[2,4,3...

24320
来自专栏阮一峰的网络日志

Pointfree 编程风格指南

本文要回答一个很重要的问题:函数式编程有什么用? 目前,主流的编程语言都不是函数式的,已经能够满足需求。为何还要学函数式编程呢,只为了多理解一些新奇的概念? ?...

36770
来自专栏一英里广度一英寸深度的学习

并查集路径压缩

并查集中的节点只需要保存父亲节点的信息,那么线性结构字典、列表都可以。我们用一维数组,索引是自身id,值指向父亲。 初始化时每个节点指向自身。

31620
来自专栏数据结构与算法

P1965 转圈游戏

题目描述 n 个小伙伴(编号从 0 到 n-1)围坐一圈玩游戏。按照顺时针方向给 n 个位置编号,从0 到 n-1。最初,第 0 号小伙伴在第 0 号位置,第 ...

39880
来自专栏运维小白

linux基础(day28)

9.6 awk(上) awk工具 head -n2 test.txt|awk -F ':' '{print $1}' head -n2 test.txt|awk...

22260
来自专栏Java程序员的架构之路

一图胜千言,8 张图理解 Java

一图胜千言,下面图解均来自Program Creek 网站的Java教程,目前它们拥有最多的票选。如果图解没有阐明问题,那么你可以借助它的标题来一窥究竟。

9010
来自专栏Python小屋

详解Python中的序列解包(2)

8个月前曾经发过一篇关于序列解包的文章,见详解Python序列解包,本文再稍作补充。 可以说,序列解包的本质就是把一个序列或可迭代对象中的元素同时赋值给多个变量...

37350
来自专栏Redis源码学习系列

Redis源码学习之字典

在字典结构体中,包含了一组字典函数(dictType),通过封装的方法处理对应的操作,通常在字典初始化的时候对其进行配置。

37410
来自专栏mathor

LeetCode410. 分割数组的最大值

 这道题看着好像没什么思路,但其实可以利用二分法来做,二分法中的mid就是最终要返回的值,也就代表着子数组的和最小的值  我们首先还是设置左右区间,左区...

15430

扫码关注云+社区

领取腾讯云代金券