海量信息即大规模数据,随着互联网技术的发展,互联网上的信息越来越多,如何从海量信息中提取有用信息成为当前互联网技术发展必须面对的问题。
在海量数据中提取信息,不同于常规量级数据中提取信息,在海量信息中提取有用数据,会存在以下几个方面的问题:
(1)数据量过大,数据中什么情况都可能存在,如果信息数量只有20条,人工可以逐条进行查找、比对,可是当数据规模扩展到上百条、数千条、数亿条,甚至更多时,仅仅只通过手工已经无法解决存在的问题,必须通过工具或者程序进行处理。
(2)对海量数据信息处理,还需要有良好的软硬件配置,合理使用工具,合理分配系统资源。通常情况下,如果需要处理的数据量非常大,超过了TB级,小型机、大型工作站是要考虑的,普通计算机如果有好的方法也可以考虑,如通过联机做成工作集群。
(3)对海量信息处理时,要求很高的处理方法和技巧,如何进行数据挖掘算法的设计以及如何进行数据的存储访问等都是研究的难点。
针对海量数据的处理,可以使用的方法非常多,常见的方法有Hash法、Bit-map法、Bloom filter法、数据库优化法、倒排索引法、外排序法、Trie树、堆、双层桶法以及MapReduce法。
Hash法
Hash 一般被翻译为哈希,也被称为散列,它是一种映射关系,即给定一个数据元素,其关键字为key,按一个确定的哈希函数Hash计算出hash(key),把hash(key)作为关键字key对应元素的存储地址(或称哈希地址),再进行数据元素的插入和检索操作。简而言之,哈希函数就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
哈希表是具有固定大小的数组,其中,表长(即数组的大小)应该为质数。哈希函数是用于关键字与存储地址之间的一种映射关系,但是不能保证每个元素的关键字与函数值是一一对应的,因为极有可能出现对应于不同的元素,却计算出了相同的函数值。冲突是指两个关键字映射到同一个存储地址的情况,即对不同的关键字可能得到同一散列地址,即key1!=key2,而 fkey1) = fkey2)
哈希函数的特点
哈希函数一般应具备以下几个方面的特点:
(1)运算应该尽可能简单
(2)函数的值域必须在散列表的范围内
(3)尽可能的减少冲突
哈希函数的构建方法
哈希函数的构建方法一般有以下几种:
(1)直接寻址法
取关键字或关键字的某个线性函数值为散列地址。即hkey)=key或hkey)=a*key+b,其中a和b均为整型常数,这种散列函数叫做自身函数。例如,有一个人口数字统计表,人的年龄取值范围为1~100岁,其中,年龄作为关键字,哈希函数取关键字自身,但这种方法效率比较低,时间复杂度O1),空间复杂度为On),n为关键字的个数。
直接寻址法不会产生冲突,但由于它没有压缩映像,因此当关键字集合很大时,使用这种Hash函数是不可能实现地址编码的散列的。
(2)取模法
选择一个合适的正整数p,令hash(Key) = key mod p。p如果选择的是比较大的素数,则效果比较好,一般选取p为TableSize,即哈希表的长度。
(3)数字分析法
设关键字是d位的以r为基的数(如以10为基的十进制数),且共有n个关键字。则关键字的每个位可能有r个不同的数符出现(即0,1,2,。。。,9),但这r个数符在各个位上出现的频率不一定相同,可能在某些位上分布比较均匀,即每个数符出现的次数接近于n/r,而在另一些位上分布不均匀。因此可选取其中分布较均匀的,重新组成新的数,用其作为哈希地址。
这种方法简单、直观,但是需要预先知道每个关键字的情况,这就限制了它的使用范围。
(4)折叠法
将关键字分成位数为t的几个部分(最后一部分的位数可能小于t),然后把各部分按位对齐进行相加,将所得的和舍弃进位,留下t位作为哈希地址。当关键字位数很多,而且关键字中每位上数字分布比较均匀时,采用折叠法比较合适。
(5)平方取中法
这是一种较常用的方法,将关键字进行平方运算,然后从结果的中间取出若干位(位数与散列地址的位数相同),将其作为散列地址,具体取几位由哈希表的表长决定。
(6)除留余数法
除留余数法是一种比较常用的哈希函数,它的主要原理是取关键字除以某个数p(p不大于哈希表的长度TableSize)的余数作为哈希地址,即Hash(key)=key%p
使用除留余数法时,选取合适的p值很重要,一般要求p<=TableSize,且接近TableSize,p一般选取质数,也可以是不包含小于20质因子的合数
(7)随机数法
选择一个随机函数,然后用关键字key的随机函数值作为哈希地址,即Hash(key)=random(key)
解决冲突的方法
解决冲突的主要途径是当一个关键字映射到哈希表中的某一个地址且该地址上已有关键字时,再为该关键字寻找新的存储地址。
(1)开放地址法
基本思想是当发生地址冲突时,在哈希表中再按照某种方法继续探测其他的存储地址,直到找到空闲的地址为止
Hikey)=Hkey)+di)mod mi = 1,2,...,k(k<=m-1))
其中Hkey)为关键字key的直接哈希地址,m为哈希表的长度,di为每次再探测时的地址增量。
采用这种方法时,首先计算出关键字的直接哈希地址,即H(key),如果该直接哈希地址上已有其他的关键字,则继续查看地址为[Hkey)+di]的存储地址,判断其是否为空。如此反复直至找到空闲的存储地址为止,然后将关键字key存放到该地址。
(2)链地址法
链地址法解决冲突的主要思想是:如果哈希表空间为[0,m-1],则设置一个由m个指针组成的一维数组CH[m],然后在寻找关键字哈希地址的过程中,所有哈希地址为i的数据元素都插入到头指针为CH[i]的链表中。这种方法比较适合于冲突比较严重的情况下使用
(3)再散列法
当发生冲突时,使用第二个、第三个哈希函数计算地址,直到无冲突时。但这种方法的缺点是计算时间会大幅增加。
(4)建立一个公共溢出区。
假设哈希函数的值域为[0,m-1],则设向量HashTable[0,…,m-1]为基本表,另外设立存储空间向量OverTable[0,…,v]用以存储发生冲突的记录。
Hash主要用来进行“快速存取”,在O1)时间复杂度里就可以查找到目标元素,或者判断其是否存在。Hash数据结构里的数据对外是杂乱无序的,无法得知其具体存储位置,也不知道各个存储元素位置之间的相互关系,但是却可以在常数时间里判断元素位置及存在与否。
在海量数据处理中,使用hash方法一般可以快速存取、统计某些数据,将大量数据进行分类。例如,提取某日访问网站次数最多的IP地址等。
Bit-map法
Bit-map(位图)法的基本原理是使用位数组来表示某些元素是否存在,如8位电话号码中查重复号码,它适用于海量数据的快速查找、判重、删除等。
如,1101001011 表示的集合为{ 1,2,4,7,9,10 }
位图排序的时间复杂度是On)的,比一般的排序都快,但它以空间换时间(需要一个N位别的串)的,而且有些限制,即数据状态不是很多。例如,排序前集合大小最好已知,而且集合中元素的最大重复次数必须已知,最好是惆集数据(不然空间浪费很大)
位图法适用于判断数据是否重复,也使用位图法判断某个数据是否存在。(需要两次遍历数据)
Bloom filter法
遇到问题:程序中判断一个元素是否在一个集合中
最直接解决方法是将集合中全部的元素都存储在计算机中,每当遇到一个新元素时,就将它和集合中的元素直接进行比较即可。这种做法虽然能够准确无误地完成任务,但是比较次数太多,效率比较低下。
Bloom filter正是解决这一问题的有效方法,它是一种空间效率和时间效率很高的随机数据结构,它用来检测一个元素是否属于一个集合。但是它牺牲了正确率,Bloom filter以牺牲正确率为前提,来换取空间效率与时间效率的提高。当它判断某元素不属于这个集合时,该元素一定不属于这个集合;当它判断某元素属于这个集合时,该元素不一定属于这个集合。具体而言,查询结果由两种可能,即“不属于这个集合(绝对正确)”和”属于这个集合(可能错误)“。所以,Bloom filter适合应用在对于低错误率可以容忍的场合。
So,使用Bloom filter的难点是如何根据输入元素个数n,来确定位数组m的大小以及hash函数。当hash函数个数 k=ln2)*m/n)
时错误率最小,在错误率不大于E的情况下,m至少要等于n*lg1/E)
才能表示任意n个元素的集合。但m还应该更大些,因为还要保证位数组里至少一半为0,则m应该>=nlg1/E)*lge
,大概就是nlg1/E)
的1.44倍(lg表示以2为底的对数)。
例如,假设E为0.01,即错误率为0.01,则此时m应该大约为n的13倍。这样k大约是8个(注意,m与n的单位不同,m的单位是bit,而n则是以元素个数为单位)。通常单个元素的长度都是有很多bit的,所以使用bloom filter内存上通常都是节省的。
Bloom filter的优点是具有很好的空间效率和时间效率。它的插入和查询时间都是常数,另外它不保存元素本身,具有良好的安全性。然而,这些优点都是以牺牲正确率为代价的。当插入的元素越多,错判“元素属于这个集合”的概率就越大。另外,Bloom filter只能插入元素,却不能删除元素,因为多个元素的哈希结果可能共用了Bloom filter结构中的同一个位,如果删除元素,就可能会影响多个元素的检测。所以,Bloom filter可以用来实现数据字典、进行数据的判重或者集合求交集。
CBF与SBF是BF的扩展,Counting Bloom Filter(CBF)将位数组中的每一位扩展为一个counter,从而支持了元素的删除操作。Spectral Bloom Filter(SBF)将其余集合元素的出现次数关联,SBF采用counter中的最小值来近似表示元素的出现频率。
数据库优化法
互联网上的数据一般都被存储在数据库中,很多情况下,人们并非对这些海量数据本身感兴趣,而是需要从这些海量数据中提取出对自己有用的信息。例如,从数据中获取访问最多的页面信息等,这就涉及数据的查询技术等相关内容。
数据库管理软件选择是否合理、表结构涉及是否规范、索引创建是否恰当都是影响数据库性能的重要因素。所以,对数据库进行优化,是实现海量数据高效处理的有效方法之一。常见的数据库优化方法有以下几种:
(1)优秀的数据库管理工具
选择一款优秀的数据库管理工具非常重要。现在的数据库一般使用Oracle、DB2、MySQL等。
(2)数据分区
进行海量数据的查询优化,一种重要方式就是如何有效地存储并降低需要处理的数据规模,所以可以对海量数据进行分区操作提高效率。例如,针对按年份存取的数据,可以按年进行分区,不同的数据库有不同的分区方式,不过处理机制却大体相同。例如,SQL Server的数据库分区是将不同的数据存于不同的文件组下,而不同的文件组存于不同的磁盘分区下,这样将数据分散开,减小磁盘I/O,减小了系统负荷,而且还可以将日志、索引等放于不同的分区下。
(3)索引
索引一般可以加速数据的检索速度,加速表与表之间的链接,提高性能,所以在对海量数据进行处理时,考虑到信息量比较大,应该对表建立索引,包括在主键上建立聚簇索引,将聚合索引建立在日期列上等。
索引优点很多,但是对于索引的建立,还需要考虑到实际情况,而不是对每一个列建立一个索引。例如,针对大表的分组、排序等字段,都要建立相应的索引,同时还应该考虑建立复合索引。增加索引同时也有很多不利的方面:
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加
- 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间。如果要建立聚簇索引,那么需要的空间就会更大。
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度。
所以索引要用到好的时机,索引的填充因子和聚集、非聚集索引都要考虑。
(4)缓存机制
当数据量增加时,一般的处理工具都要考虑到缓存问题。缓存大小设置的好差也关系到数据处理的成败。例如,在处理2亿条数据聚合操作时,缓存设置为10万条/Buffer可行。
(5)加大虚存
由于系统资源有限,而需要处理的数据量非常大,所以当内存不足时,可以通过增加虚拟内存来解决
(6)分批处理
由于需要处理的信息量巨大,可以对海量数据进行分批处理(类似于云计算中的MapReduce思想),然后再对处理后的数据进行合并操作,分而治之,有利于小数据量的处理,不至于面对大数据量带来的问题。
(7)使用临时表和中间表
数据量增加时,处理中要考虑提前汇总。这样做的目的是化整为零,大表变小表,分块处理完成后,再利用一定的规则进行合并,处理过程中的临时表的使用和中间结果的保存都非常重要。如果对于超海量的数据,大表处理不了,只能拆分为多个小表。如果处理过程中需要多步汇总操作,可按汇总步骤一步步来。
(8)优化查询语句
查询语句的性能对查询效率的影响是非常大的。编写高效优良的SQL脚本和存储过程是数据库工作人员的职责,也是检验数据库工作人员水平的一个标准。
(9)使用视图
视图中的数据来源于基本表,对海量数据的处理,可以将数据按一定的规则分散到各个基本表中,查询或处理过程中可以基于视图进行。
(10)使用存储过程
在存储过程中尽量使用SQL自带的返回参数,而非自定义的返回参数,减少不必要的参数,避免数据冗余。
(11)用排序来取代非顺序存取
磁盘存取臂的来回移动使得非顺序磁盘存取变成了最慢的操作,但是在SQL语句中这个现象被隐藏了,这样就使得查询中进行了大量的非顺序页查询,降低了查询速度。
(12)使用采样数据进行数据挖掘
基于海量数据的数据挖掘正在逐步兴起,面对着超海量的数据,一般的挖掘软件或算法往往采用数据抽样的方式进行处理,这样的误差不会很高,大大提高了处理效率和处理的成功率。一般采样时要注意数据的完整性,防止过大的偏差。
倒排索引法
倒排索引是目前搜素引擎公司对搜索引擎最常用的存储方式,也是搜索引擎的核心内容。在搜索引擎实际的引用之中,有时需要按照关键字的某些值查找记录,所以是按照关键字建立索引,这个索引就被称为倒排索引。
倒排索引也常被称为反向索引、置入档案或反向档案,它本质上是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。它是文档检索系统中最常用的数据结构,有两种不同的反向索引形式:
(1)一条记录的水平反向索引(或者反向档案索引)包含每个引用单词的文档的列表
(2)一个单词的水平反向索引(或者完全反向索引)又包含每个单词在一个文档中的位置。
第二种形式提供了更多的兼容性(如短语搜索),但是需要更多的时间和空间来创建。
一般情况下可以采用矩阵的方式来存储,但会浪费大量的空间。例如,对于如下的内容,
D1:The GDP increased.
D2:The text is this.
D3:My name is.
如果采用矩阵的方式存储,见表14-1.其中,行表示关键字,列表示所有的文件。
通过比较发现,采用倒排索引比采用矩阵的方式节省很多的空间。
正向索引开发出来用来存储每个文档的单词的列表。正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文档中的验证查询。在正向索引中,文档占据了中心的位置,每个文档指向了一个它所包含的索引项的序列。也就是说,文档指向了它包含的那些单词,而反向索引则是单词指向了包含它的文档,很容易看到这个反向的关系。而与正向索引相比,倒排索引的优点是在处理复杂的多关键字查询时,可在倒排表中先完成查询的并、交等逻辑运算,得到结果后再对记录进行存取,这样不必对每个记录随机存取,把对记录的查询转换为地址集合的运算,从而提高查找速度。所以,倒排索引一般被应用于文档检索系统,查询哪些文件包含了某个单词,比如常见的学术论文的关键字搜索
外排序法
外排序,即当待排序的对象数目特别多时,在内存中不能一次处理,必须把它们以文件的形式存放于外存,排序时再把它们一部分一部分调入内存进行处理。
外排序是相对内排序而言的,它是大文件的排序,待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。一般采用归并排序等方式实现外排序,主要分为两个步骤
(1)生成若干初始归并段(顺串),也被称为文件预处理,把含有n个记录的文件,按内存大小划分为若干长度为L的子文件,然后分别将子文件调入内存,采用有效的内排序方法排序后送回外存
(2)进行多路归并,即对这些初始归并段进行归并,使得有序的归并段逐渐扩大,最后再外存上形成整个文件的单一归并段,此时就完成了文件的外排序。
外排序的适用范围是大数据的排序以及去重复。但外排序也存在着很大的缺陷,就是它会消耗大量的IO,效率不会很高。
Trie树
Trie 这个单词来自于“retrieve”,Trie树又称字典树或键树。是一种用于快速字符串检索的多叉树结构,其原理是利用字符串的公共前缀来降低时空开销,即以空间换时间,从而达到提高程序效率的目的。Trie树的典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie树一般具有以下3个基本特性:
(1)根结点不包含字符,除根结点外每一个结点都只包含一个字符
(2)从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的字符串
(3)每个结点的所有子结点包含的字符都不相同。
Trie树可以利用字符串的公共前缀来节约存储空间。 如图14-4所示,该Trie树用10个结点保存了5个字符串 amy、aan、em、rob、rg
在该Trie树中,字符串“amy”和“aan”有公共前缀“a”。当然,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的Trie树将非常消耗内存,这也是Trie树的一个缺点。
给一个单词a,如果通过交换单词中字母的顺序可以得到另外的单词b,那么称b是a的兄弟单词。例如,单词army和mary互为兄弟单词。现在要给出一种解决方案,对于用户输入的单词,根据给定的字典找出输入单词有哪些兄弟单词。
一般情况下,Trie树的结构都是采用26叉树进行组织的,每个结点对应一个字母,查找的时候,就是一个字母一个字母地进行匹配,算法的时间复杂度就是单词的长度n,效率很高。本例子可以定义一个Trie树作为数据结构来查询,此时就转化为在一棵Trie树中查找兄弟单词,只要在Trie树中的前缀中再存储一个vector结构的容器,就可以大大降低时间复杂度。
上例中,Trie树的构建是在预处理阶段完成的,首先根据字典中的单词来建立字典树,当建立完字典树后,查询兄弟单词的效率就会提高很多,比hash法效率还要高。
Trie树适用数据量大、重复多,但是数据种类小可以放入内存的情况。例如,已知n(n很大)个由小写字母构成的平均长度为10的单词,判断其中是否存在某个字符串是另一个字符串的前缀子串。针对这种问题,一般可以采用以下3种方法。
(1)迭代法
对于每一个单词,都要去查找它前面的单词中是否包含它,看每个字符串是否为字符串集中某个字符串的前缀,由于需要不停地进行迭代比较,所以此时的时间复杂度为On^2)
(2)Hash法
使用Hash方法存储所有字符串的所有前缀子串。而建立存有子串Hash的时间复杂度为On*len),查询的复杂度为On)*O1)=On)
(3)Trie树
假设要查询的单词是abcd,那么在它前面的单词中,以b、c、d、f之类开头的单词则不必考虑,而只要找以a开头的单词中是否存在abcd就可以了。同样,在以a开头的单词中,只要考虑以b作为第二个字母的单词即可,所以建立Trie树的复杂度为On*len),而建立操作与查询操作在trie树中是可以同时执行的。所以,总的时间复杂度为On*len),实际查询的复杂度只是Olen)。
例如,有串911,911456输入,如果要同时执行建立与查询,过程如下:首先查询911,没有;然后存入9、91、911,再查询911456,没有;然后存入9114、91145、911456,而程序没有记忆功能,并不知道911在输入数据中出现过,所以使用Hash必须先存入所有子串,然后for循环查询。而trie树则可以,存入911后,已经记录911为出现的字符串,在存入911456的过程中就能发现而输出答案。反过来也可以,县存入911456,再存入911时,当指针指向最后一个1时,程序会发现这个1已经存在,说明911必定是某个字符串的前缀
堆
堆是一种树形数据结构,每个结点都有一个值,而通常所说的堆,一般是指二叉堆。在堆中,以大顶堆为例,堆的根结点的值最大,且根结点的两个子树也是一个大顶堆,基于以上特点,堆适用于海量数据求前N大(用小顶堆)或者前N小(用大顶堆)数问题,其中N一般比较小。例如,当求海量数据前N小的数据时,使用大顶堆,比较当前元素与大顶堆的最大元素(即堆顶元素),如果该元素小于最大元素,则应该替换该最大元素,并调整堆的结构。当求海量数据前N大的数据时,思路一样。由于采用堆,只需要扫描一遍即可得到所有的前n元素,所以在海量信息处理中,效率非常高。
双层桶法
双层桶不是一种数据结构,而是一种算法思想,类似于分治思想。因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,最后在一个可以接受的范围内进行。
本文以桶排序进行分析,桶排序的基本思想是把[ 0,1)划分为n个大小相同的子区间,每一子区间是一个桶,然后将n个记录分配到各个桶中。因为关键字序列是均匀分布在 [ 0,1)上的,所以必须采用关键字比较的排序方法(通常用插入排序)对各个桶进行排序,然后依次将各非空桶中的记录连接(收集)起来即可。这种排序思想的前提是假设输入的n个关键字序列随机分布在区间 [ 0,1)之上,若关键字序列的取值范围不是该区间,只要其取值均非负,总能将所有关键字除以某一合适的数,将关键字映射到该区间上,但要保证映射后的关键字是均匀分布在 [ 0,1)上的。
桶排序的平均时间复杂度是On),最坏情况仍有可能是On^2),一般只适用于关键字取值范围较小的情况,否则所需桶的数目m太多导致浪费存储空间和计算时间。例如,n=10,被排序的记录关键字ki取值范围是0~99之间的整数(36,5,16,98,95,47,32,36,48)时,要用100个箱子来做一趟排序。
桶排序一般适用于寻找第k大的数、寻找中位数、寻找不重复或重复的数字等情况。例如:
(1)在一个文件中有10亿个整数,乱序排列,要求找出中位数,内存限制2GB
(2)现在有一个0~30000的随机数生成器。请根据这个随机数生成器,设计一个抽奖范围是0~350000彩票中奖号码列表,其中要包含20000个中奖号码。
MapReduce
MapReduce是云计算的核心技术之一,是一种简化并行计算的分布式编程模型。它为并行系统的数据处理提供了一个简单、高效的解决方案,其主要目的是为了大型集群的系统能在大数据集上进行并行工作,并用于大规模数据的并行运算。
MapReduce适用于大规模数据集(通常大于1TB)的并行运算,它的核心操作是Map和Reduce,即MapReduce拆开为 “Map(映射)”和“Reduce(化简)”。其中,Map函数独立地对每个元素进行操作,它用于把一组键值对映射成一组新的键值对,即先通过Map程序将数据切割成不相关的区块,分配(调度)给大量计算机处理达到分布计算的效果,然后通过指定并发的Reduce函数来将结果汇总,保证所有映射键值对中的每一个共享相同的键组。
简而言之,一个映射函数就是对一些独立元素组成的概念上的列表(如一个测试成绩的列表)的每一个元素进行指定的操作(例如,有人发现所有学生的成绩都被低估了一分,它可以定义一个“加1”的映射函数,用来修正这个错误)。而Map操作与Reduce操作都可以高度并行运行,Map是把一组数据一对一地映射为另外的一组数据,其映射的规则由一个函数来指定。例如,对 [ 1,2,4,8 ] 进行乘 2 的映射就变为 [ 2,4,8,16 ] ,Reduce是对一组数据进行规约,这个规约的规则是由另外一个函数指定的。例如,对 [ 1,2,4,8 ] 进行求和规约得到的结果是 15,而对它进行求积的规约是 64。
通过MapReduce,不会分布式并行编程的程序员也能很容易地将自己的程序运行在分布式系统上。同时,通过该模型,能够充分高效地利用集群中每个机器的资源,适合在集群中处理大规模数据的计算任务,这些优点使得其已成为云计算平台的主流编程模型。