前言
本次博文对何昊出版的《java程序员面试宝典》的第9章海量数据部分的概括笔记,删除部分内容,是本系列笔记博客的最后一个博文。
相关知识
Hash法
映射关系,给定数据元素,关键字是key,按照确定散列函数计算hash(key),然后该值作为关键字key对应元素的存储位置,再进行数据插入和检索。
散列函数是将任意长度信息压缩为固定长度信息摘要函数。
散列表具有固定大小数组,表长(数组大小)为质数。散列函数是关键字与存储地址间映射关系,不能保证一一对应,会发生hash冲突,即两个关键帧不同丹映射同一存储地址。
hash函数具备特点:运算尽量简单;值域必须在散列表范围;尽可能减少冲突;
(1)直接寻址
取关键字或关键字某个线性函数为散列地址,即h(key)=key或h(key)=a*key+b,a和b均为整型常数。
时间复杂度 O ( 1 ) \mathcal{O(1)} O(1),空间复杂度 O ( n ) \mathcal{O(n)} O(n)
不会产生冲突,因为没有压缩映像,关键字集合大,这种方式不可取。
(2)取模法
取合适正整数,hash(key)=key mod p。p是比较大素数比较好,一般取TableSize,即散列表长。
(3)数字分析法
关键字是d位以r为基的数(例如10为基的十进制),且共有n个关键帧,则关键帧的每个位可能有r个不同的数符(即0,1,2,…,9),但这r个数符在各个位上出现的频率不一定相同,可能在某些位上分布比较均匀,即每个数符出现的次数接近于 n / r n/r n/r,而另一些位上分布不均匀。因此取其中分布均匀的那些位组成新数,用作散列地址。
方法直观简单,但需要预先知道每个关键字情况,限制使用范围。
(6)折叠法
将关键字分为位数为1的几个部分(最后一部分的位数可能小于1),然后把各部分按位对齐进行相加,将所得的和舍弃进位,留下 t t t 位作为散列地址。当关键字位数很多,而且关键字中每位上数字分布比较均匀,采用折叠法合适。
(5)平方取中法
将关键字进行平方运算,然后从结果中间取出若干位(位数与散列地址的位数相同),将其作为散列地址,具体取几位,由散列表表长决定。
(6)除留余数法
取关键字除以某个数p(p不大于散列表长度)的余数作为散列地址, H a s h ( k e y ) = k e y % p Hash(key)=key\%p Hash(key)=key%p
p值很重要,一般小于表长,且接近或等于。一般是选质数,可以是不包含小于20质因子的合数。
(7)随机数法
hash(key)=random(key)当关键字长度不相等,采用此法。
冲突难以避免,解决hash冲突主要是当一个关键字映射后的地址若已有关键字则为该关键字重新寻找新的存储地址。
(1)开放地址法
地址发生冲突,hash表再按照某种方法继续探测其他开放地址,直到找到空闲地址为止。
H i ( k e y ) = ( H ( k e y ) + d i ) m o d m ( i = 1 , 2 , . . . , k ( k < = m − 1 ) ) H_i(key)=(H(key)+d_i) mod \quad m (i=1,2,...,k(k<=m-1)) Hi(key)=(H(key)+di)modm(i=1,2,...,k(k<=m−1))
其中, H ( k e y ) H(key) H(key)为关键字key的直接散列地址, m m m为散列表的长度, d i d_i di为每次再探测的地址增量
该方式先找到直接H(key),若该地址已有其他关键字,则继续查看地址为 [ H ( k e y ) + d i ] [H(key)+d_i] [H(key)+di]的存储地址,判断其是否为空。如此反复,直到找到空闲的存储地址为止,然后将关键帧key存放到该地址。
d i d_i di的取法:
1) d i = 1 , 2 , 3 , . . . , m − 1 d_i=1,2,3,...,m-1 di=1,2,3,...,m−1,称为线性探测再散列
2) d i = 12 , − 12 , 22 , − 22 , . . . , − k 2 ( k < = m / 2 ) d_i=12,-12,22,-22,...,-k2(k<=m/2) di=12,−12,22,−22,...,−k2(k<=m/2),称为二次探测再散列
3) d i = d_i= di=伪随机序列,称为伪随机再散列
利用此法解决冲突的散列表,删除元素不能直接删除(会影响其他相同散列地址元素),采用特殊标记表示已被删除。
(2)链地址法
若散列表空间为 [ 0 , m − 1 ] [0,m-1] [0,m−1],则设置一个由m个指针组成的一维数组 C H [ m ] CH[m] CH[m],好在寻找关键字散列地址过程中,所有散列地址为 i i i的数据元素都插入到头指针为 C H [ i ] CH[i] CH[i]的链表中。
适用于冲突严重情况。
(3)再散列
发生冲突,使用第二个、第三个、散列函数计算地址,直到无冲突为止。
计算时间会大幅增加
(4)建立一个公共溢出区
假设散列函数值域 [ 0 , m − 1 ] [0,m-1] [0,m−1],设立存储空间向量 O v e r T a b l e [ 0 , . . . , v ] OverTable[0,...,v] OverTable[0,...,v]用以存储发生冲突记录
hash主要用来“快速存取”,在 O ( 1 ) \mathcal{O}(1) O(1)复杂度可以查到目标元素,或判其是否存在。
hash数据结构中的数据对外是杂乱无序,因此具体存储位置及各个存储元素之间的相互关系无法得知,但可在常数时间判断元素位姿和存在与否。
hash法一般可以快速存取和统计某些数据,将大量数据分类。
Bit-map法
使用数组表示某些元素存在与否。适用于海量数据快速查找,判重,删除等。
该法生成一个N位长串,每位以“1”或“0”表示需要排序的集合中的数。
时间复杂度 O ( n ) \mathcal{O(n)} O(n),以空间换时间,要求数据状态不多。
适用于数据量大,判断是否重复问题或集合某数据是否存在。
Bloom Filter法
以牺牲正确率,一种空间小和时间效率都很高的随机数据结构,用来检测元素是否属于集合。适用于低错误率可以容忍的场合。(不属于是绝对正确,属于可能是错误)
将位数据与Hash函数联合使用。Bloom Filter是包含m位的位数组,每位都是0。定义k不同的hash函数,每个函数可以将集合元素映射到位数组某一位。当集合插入元素,根据k个hash函数可以得到位数组k个位,将这些位设置为1。
如果查询某元素属于集合,则根据k个hash函数得到位数组k个位,查看k个位中值,有的位不为1,则元素肯定不在集合中;若全为1,则可能存在。在插入其他元素,可能把这些元素位置设为1,产生错误。
难点在于如何根据输入元素个数n确定位数组m的大小以及hash函数。当hash函数个数 k = ( ln 2 ) ∗ ( m / 2 ) k=(\ln2)*(m/2) k=(ln2)∗(m/2)时错误率最小,在错误率不大于E的情况下,m至少等于 n ∗ log ( 1 / E ) n*\log(1/E) n∗log(1/E)才能表示任意n个元素的集合。但m还应更大,因为至少保证位数组里至少一半为0,所以m应该 ≥ n ∗ lg ( 1 / E ) ∗ lg ( e ) \ge n*\lg(1/E)*\lg (e) ≥n∗lg(1/E)∗lg(e)。大概就是 n ∗ lg ( 1 / E ) n*\lg(1/E) n∗lg(1/E)的1.44倍,lg表示以2为底的对数。
通常单元素长度有很多bit,使用该法在内存通常是节省的。
该法优点在空间效率和时间效率。在插入和查询都是常量时间,不保存元素本身具有良好安全,但都以牺牲正确率为代价。当插入元素越多,判断“元素输入该集合”概率越大。另外只能插入元素而不能删除元素,因为多个元素散列结果可能共用同一位。如果删除,会影响多个元素检测。
使用该法实现数据字典、进行数据判重和集合求交集。
CBF和SBF是该方法扩展,前者在每位扩展counter用以支持元素删除,后者与集合元素次数关联,使用counter最小值近似表示元素的出现频率。
数据库优化法
(1)优秀数据库管理工具(DB2,MySQL等)
(2)数据分区。
(3)索引。
(4)缓存机制
(5)加大虚存
(6)分批处理(分而治之,类似MapReduce),利用小数据量
(7)使用临时表和中间表。
(8)优化查询语句
(9)使用视图
(10)使用存储过程
(11)用排序来取代非顺序存取
(12)使用采样数据进行数据挖掘
倒排索引法
指按照关键字建立索引。又称为反向索引,置入档案或反向档案。
被用来存储在全文搜索下某个单词在一个文档中的存储位置映射,它是文档检索系统中最常用的数据结构,有两种不同反向索引形式。第一种形式是一条记录的水平反向索引(或反向档案索引)包含每个引用单词的文档的列表;第二种形式是一个单词的水平反向索引(或完全反向索引)又包含每个单词在一个文档中的位置。第二种形式提供了更多兼容性(如短语搜索),但需要更多时间和空间创建。
一般采用矩阵方式存储来存储会浪费大量空间。
倒排索引比采用矩阵方式节省很多空间。
正向索引用来存储每个文档的单词列表。正向索引查询往往满足每个文档有序频繁的全文查询和每个单词在校验文档中的验证这样查询。在正向索引中,文档占据了中心位置,每个文档指向一个它所包含索引项。
文档指向它包含的单词,而反向索引则单词指向包含它的文档。
倒排索引优点,处理复杂多关键字查询,可在倒排表完成查询并交逻辑,得到结果对记录进行存取,这样把记录查询转换为地址集合运算,而不必对每个记录随机存取,故而提高效率。
外排序法
当待排序的对象数目特别多,在内存中不能一次处理,必须把它们以文件的形式存放于外存,排序时再把它们一部分一部分地调入内存进行处理,这种方式就是外排序。
大文件排序,待排序记录存储在外存储器说,带排序文件无法一次装入内存。需要在内存和外部存储器之间多次数据交换,以达到对整个文件进行排序目的。一般采用归并排序等方式实现外排序。
分两步,第一步生成若干归并段(顺串),也称为文件预处理,把含有n个记录的文件,按内存大小划分为若干长度为L的子文件,然后分别把子文件调入内存,采用有效的内排序方法排序后送回内存。
第二步,进行多路归并,即对这初始归并段进行多便归并,并使得有序的归并段逐渐扩大,最后在外存上形成整个文件的单一归并段,也就是完成了文件的外排序。
外排序适用于大数据的排序以及去重复,但外排序也存在很大缺陷,会耗大量IO,效率不高。
Trie树
字典树或键树,一种快速字符串检索多叉树结构,利用字符串的公共前缀减少时空开销,以空间换时间,达到提高程序效率目的。
典型用于统计和排序大量字符串(不限于字符串),用于文本词频统计。
能最大限度减少无谓字符串比较,查询效率比散列表高。
三个基本特性:
(1) 根节点不包含字符,除根节点外每个节点只包含一个字符。
(2)从根节点到某一节点,经过路径的字符连接即为结点对应字符串;
(3)每个结点的所有子结点包含字符都不相同。
若是大量字符串无公共前缀,则耗费内存。
适用于数据量大,重复多,数据种类小可放入内存情况。
双层捅
算法思想,分而治之。因元素范围大,不能直接寻址,所以多次划分,逐步确定范围,最后在一个可接受范围进行。
top K问题
找出频率最高前K个数,最大的前K数。
最好是分冶+Trie树/hash+小顶端。
将数据集按照hash方法分解为多个小数据集,然后使用Trie树或hash统计对每个小数据集的query词频,然后用小顶端求出每个数据集频率最高的前K个数,最好在所有topK中求出最终的topK。
重复问题
一般用位图实现。
排序问题
(1)数据库排序法
(2)分冶法。
(3)位图法
结语
本系列笔记博客的最后一个博文,是对何昊出版的《java程序员面试宝典》的第9章海量数据部分的概括笔记,删除部分内容(比如堆,MapReduce等),有需可以购买该书或者查询该处内容,我觉得这两个地方比较简单易懂就不整理了。