海量处理数据的算法总结文档格式.docx
《海量处理数据的算法总结文档格式.docx》由会员分享,可在线阅读,更多相关《海量处理数据的算法总结文档格式.docx(25页珍藏版)》请在冰豆网上搜索。
因为该元素对应的位会牵动到其他的元素。
所以一个简单的改进就是countingBloomfilter,用一个counter数组代替位数组,就可以支持删除了。
此外,BloomFilter的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个。
注意:
这里m与n的单位不同,m是bit为单位,而n则是以元素个数为单位(准确的说是不同元素的个数)。
通常单个元素的长度都是有很多bit的。
所以使用bloomfilter内存上通常都是节省的。
一般BF可以与一些key-value的数据库一起使用,来加快查询。
由于BF所用的空间非常小,所有BF可以常驻内存。
这样子的话,对于大部分不存在的元素,我们只需要访问内存中的BF就可以判断出来了,只有一小部分,我们需要访问在硬盘上的key-value数据库。
从而大大地提高了效率。
【扩展】
Bloomfilter将集合中的元素映射到位数组中,用k(k为哈希函数个数)个映射位是否全1表示元素在不在这个集合中。
Countingbloomfilter(CBF)将位数组中的每一位扩展为一个counter,从而支持了元素的删除操作。
SpectralBloomFilter(SBF)将其与集合元素的出现次数关联。
SBF采用counter中的最小值来近似表示元素的出现频率。
【问题实例】
给你A,B两个文件,各存放50亿条URL,每条URL占用64字节,内存限制是4G,让你找出A,B文件共同的URL。
如果是三个乃至n个文件呢?
根据这个问题我们来计算下内存的占用,4G=2^32大概是40亿*8大概是340亿bit,n=50亿,如果按出错率0.01算需要的大概是650亿个bit。
现在可用的是340亿,相差并不多,这样可能会使出错率上升些。
另外如果这些urlip是一一对应的,就可以转换成ip,则大大简单了。
2.Hash
【什么是Hash】
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射,pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。
这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。
简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
HASH主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做HASH值.也可以说,hash就是找到一种数据内容和数据存放地址之间的映射关系。
数组的特点是:
寻址容易,插入和删除困难;
而链表的特点是:
寻址困难,插入和删除容易。
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?
答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,(也是树的一种存储结构,称为二叉链表)我们可以理解为“链表的数组”,如图:
左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。
我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
元素特征转变为数组下标的方法就是散列法。
散列法当然不止一种,下面列出三种比较常用的:
1,除法散列法(求模数)
最直观的一种,上图使用的就是这种散列法,公式:
index=value%16
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。
2,平方散列法
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。
公式:
index=(value*value)>
>
28
如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元素的值算出来的index都是0——非常失败。
也许你还有个问题,value如果很大,value*value不会溢出吗?
答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获取相乘结果,而是为了获取index。
3,斐波那契(Fibonacci)散列法
平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?
答案是肯定的。
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
这几个“理想乘数”是如何得出来的呢?
这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,如果你还有兴趣,就到网上查找一下“斐波那契数列”等关键字,我数学水平有限,不知道怎么描述清楚为什么,另外斐波那契数列的值居然和太阳系八大行星的轨道半径的比例出奇吻合,很神奇,对么?
对我们常见的32位整数而言,公式:
index=(value*2654435769)>
如果用这种斐波那契散列法的话,那我上面的图就变成这样了:
很明显,用斐波那契散列法调整之后要比原来的取摸散列法好很多。
快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。
hash函数选择,针对字符串,整数,排列,具体相应的hash方法。
碰撞处理:
一种是openhashing,也称为拉链法;
另一种就是closedhashing,也称开地址法,openedaddressing。
d-lefthashing中的d是多个的意思,我们先简化这个问题,看一看2-lefthashing。
2-lefthashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,h1和h2。
在存储一个新的key时,同时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。
这时需要检查T1中的h1[key]位置和T2中的h2[key]位置,哪一个位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。
如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key存储在左边的T1子表中,2-left也由此而来。
在查找一个key时,必须进行两次hash,同时查找两个位置。
1).海量日志数据,提取出某日访问XX次数最多的那个IP。
IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。
3.Bit-map
【什么是Bit-map】
所谓的Bit-map就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。
由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。
如果说了这么多还没明白什么是Bit-map,那么我们来看一个具体的例子,假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复)。
那么我们就可以采用Bit-map的方法来达到排序的目的。
要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0(如下图:
)
然后遍历这5个元素,首先第一个元素是4,那么就把4对应的位置为1(可以这样操作p+(i/8)|(0x01<
<
(i%8))当然了这里的操作涉及到Big-ending和Little-ending的情况,这里默认为Big-ending),因为是从零开始的,所以要把第五位置为一(如下图):
然后再处理第二个元素7,将第八位置为1,,接着再处理第三个元素,一直到最后处理完所有的元素,将相应的位置为1,这时候的内存的Bit位的状态如下:
然后我们现在遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的。
下面的代码给出了一个BitMap的用法:
排序。
C代码
1.//定义每个Byte中有8个Bit位
2.
#include
<memory.h>
3.
#define
BYTESIZE
8
4.
void
SetBit(char
*p,
int
posi)
5.
{
6.
for(int
i=0;
i
<
(posi/BYTESIZE);
i++)
7.
8.
p++;
9.
}
10.
11.
*p
=
*p|(0x01<<(posi%BYTESIZE));
//将该Bit位赋值1
12.
return;
13.
14.
15.
BitMapSortDemo()
16.
17.
//为了简单起见,我们不考虑负数
18.
num[]
{3,5,2,10,6,12,8,14,9};
19.
20.
//BufferLen这个值是根据待排序的数据中最大值确定的
21.
//待排序中的最大值是14,因此只需要2个Bytes(16个Bit)
22.
//就可以了。
23.
const
BufferLen
2;
24.
char
*pBuffer
new
char[BufferLen];
25.
26.
//要将所有的Bit位置为0,否则结果不可预知。
27.
memset(pBuffer,0,BufferLen);
28.
i<9;
29.
30.
//首先将相应Bit位上置为1
31.
SetBit(pBuffer,num[i]);
32.
33.
34.
//输出排序结果
35.
i<BufferLen;
i++)//每次处理一个字节(Byte)
36.
37.
j=0;
j<BYTESIZE;
j++)//处理该字节中的每个Bit位
38.
39.
//判断该位上是否是1,进行输出,这里的判断比较笨。
40.
//首先得到该第j位的掩码(0x01<<j),将内存区中的
41.
//位和此掩码作与操作。
最后判断掩码是否和处理后的
42.
//结果相同
43.
if((*pBuffer&
(0x01<<j))
==
44.
45.
printf("
%d
"
i*BYTESIZE
+
j);
46.
47.
48.
pBuffer++;
49.
50.
51.
52.
_tmain(int
argc,
_TCHAR*
argv[])
53.
54.
BitMapSortDemo();
55.
return
0;
56.
可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下
【基本原理及要点】
使用bit数组来表示某些元素是否存在,比如8位电话号码
Bloomfilter可以看做是对bit-map的扩展
【问题实例】
1)已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数
2)8位最多99999999,大概需要99m个bit(1024*1024*99个bit),大概10几m字节的内存即可。
申请内存空间的大小为:
inta[1+N/32]=((99999999/32+1)*4个字节/1024/1024=1.2M
(可以理解为从0-99999999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话)
2)2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
将bit-map扩展一下,用2bit表示一个数即可,0表示未出现,1表示出现一次,2表示出现2次及以上,在遍历这些数的时候,如果对应位置的值是0,则将其置为1;
如果是1,将其置为2;
如果是2,则保持不变。
或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个2bit-map,都是一样的道理。
4.堆
【什么是堆】
在八大排序里面有堆的详细介绍:
八大排序算法
概念:
堆是一种特殊的二叉树,具备以下两种性质
1)每个节点的值都大于(或者都小于,称为最小堆)其子节点的值
2)树是完全平衡的,并且最后一层的树叶都在最左边
这样就定义了一个最大堆。
如下图用一个数组来表示堆:
那么下面介绍二叉堆:
二叉堆是一种完全二叉树,其任意子树的左右节点(如果有的话)的键值一定比根节点大,上图其实就是一个二叉堆。
你一定发觉了,最小的一个元素就是数组第一个元素,那么二叉堆这种有序队列如何入队呢?
看图:
假设要在这个二叉堆里入队一个单元,键值为2,那只需在数组末尾加入这个元素,然后尽可能把这个元素往上挪,直到挪不动,经过了这种复杂度为Ο(logn)的操作,二叉堆还是二叉堆。
那如何出队呢?
也不难,看图
出队一定是出数组的第一个元素,这么来第一个元素以前的位置就成了空位,我们需要把这个空位挪至叶子节点,然后把数组最后一个元素插入这个空位,把这个“空位”尽量往上挪。
这种操作的复杂度也是Ο(logn)。
【适用范围】
海量数据前n大,并且n比较小,堆可以放入内存
最大堆求前n小,最小堆求前n大。
方法,比如求前n小,我们比较当前元素与最大堆里的最大元素,如果它小于最大元素,则应该替换那个最大元素。
这样最后得到的n个元素就是最小的n个。
适合大数据量,求前n小,n的大小比较小的情况,这样可以扫描一遍即可得到所有的前n元素,效率很高。
【扩展】
双堆,一个最大堆与一个最小堆结合,可以用来维护中位数。
1)100w个数中找最大的前100个数。
用一个100个元素大小的最小堆即可。
5.双层桶
【什么是双层桶】
事实上,与其说双层桶划分是一种数据结构,不如说它是一种算法设计思想。
面对一堆大量的数据我们无法处理的时候,我们可以将其分成一个个小的单元,然后根据一定的策略来处理这些小单元,从而达到目的。
第k大,中位数,不重复或重复的数字
因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。
可以通过多次缩小,双层只是一个例子,分治才是其根本(只是“只分不治”)。
当有时候需要用一个小范围的数据来构造一个大数据,也是可以利用这种思想,相比之下不同的,只是其中的逆过程。
1).2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
有点像鸽巢原理,整数个数为2^32,也就是,我们可以将这2^32个数,划分为2^8=256个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区域,然后不同的区域在利用bitmap就可以直接解决了。
也就是说只要有足够的磁盘空间,就可以很方便的解决。
当然这个题也可以用我们前面讲过的BitMap方法解决,正所谓条条大道通罗马~~~
2).5亿个int找它们的中位数。
这个例子比上面那个更明显。
首先我们将int划分为2^16个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。
然后第二次扫描我们只统计落在这个区域中的那些数就可以了。
实际上,如果不是int是int64,我们可以经过3次这样的划分即可降低到可以接受的程度。
即可以先将int64分成2^24个区域,然后确定区域的第几大数,在将该区域分成2^20个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有2^20,就可以直接利用directaddrtable进行统计了。
3).现在有一个0-30000的随机数生成器。
请根据这个随机数生成器,设计一个抽奖范围是0-350000彩票中奖号码列表,其中要包含20000个中奖号码。
这个题刚好和上面两个思想相反,一个0到3万的随机数生成器要生成一个0到35万的随机数。
那么我们完全可以将0-35万的区间分成35/3=12个区间,然后每个区间的长度都小于等于3万,这样我们就可以用题目给的随机数生成器来生成了,然后再加上该区间的基数。
那么要每个区间生成多少个随机数呢?
计算公式就是:
区间长度*随机数密度,在本题目中就是30000*(20000/350000)。
最后要注意一点,该题目是有隐含条件的:
彩票,这意味着你生成的随机数里面不能有重复,这也是我为什么用双层桶划分思想的另外一个原因。
6.数据库索引及优化
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
数据库索引
什么是索引
数据库索引好比是一本书前面的目录,能加快数据库的查询速度。
例如这样一个查询:
select*fromtable1whereid=44。
如果没有索引,必须遍历整个表,直到ID等于44的这一行被找到为止;
有了索引之后(必须是在ID这一列上建立的索引),直接在索引里面找44(也就是在ID这一列找),就可以得知这一行的位置,也就是找到了这一行。
可见,索引是用来定位的。
索引分为聚簇索引和非聚簇索引两种,聚簇索引是按照数据存放的物