- 无法在较短时间内迅速解决;
- 无法一次性装入内存。
2,解决方法
针对时间,搭配合适的数据结构,如Bloom filter/Hash/bit-map/堆/数据库或倒排索引/trie树;
针对空间,大而化小:分而治之/hash映射。
- 分而治之/hash映射 + hash统计 + 堆/快速/归并排序;
- 双层桶划分;
- Bloom filter/Bitmap;
- Trie树/数据库/倒排索引;
- 外排序;
- 分布式处理之Hadoop/Mapreduce。
(1)分而治之/Hash映射 + Hash统计 + 堆/快速/归并排序
【适用范围】
快速查找、删除的基本数据结构,通常需要总数据量可以放入内存。
【基本原理及要点】
先映射,而后统计,最后排序:
1)分而治之/hash映射:针对数据太大,内存受限,只能是:把大文件化成(取模映射)小文件,即16字方针:大而化小,各个击破,缩小规模,逐个解决。
2)hash统计:当大文件转化了小文件,那么我们便可以采用常规的hash_map(key,value)来进行频率统计。
3)堆/快速排序:统计完了之后,便进行排序(可采取堆排序),得到次数最多的key。
【问题实例】
A. 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门),请你统计最热门的10个查询串,要求使用的内存不能超过1G。
要统计最热门查询,首先就是要统计每个Query出现的次数,然后根据统计结果,找出Top 10。所以我们可以基于这个思路分两步来设计该算法。
第一步:Query统计
Query统计有以下两个方法,可供选择:
1、直接排序法
我们可以采用外排序的方法来进行排序,这里我们可以采用归并排序,因为归并排序有一个比较好的时间复杂度O(NlgN)。
排完序之后我们再对已经有序的Query文件进行遍历,统计每个Query出现的次数,再次写入文件中。
综合分析一下,排序的时间复杂度是O(NlgN),而遍历的时间复杂度是O(N),因此该算法的总体时间复杂度就是O(N+NlgN)=O(NlgN)。
题目中说明了,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query 255Byte,因此我们可以考虑把它们都放进内存中去,而现在只是需要一个合适的数据结构,在这里,Hash Table绝对是我们优先的选择,因为Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。
那么,我们的算法就有了:维护一个Key为Query字串,Value为该Query出现次数的HashTable。
本方法相比算法1:在时间复杂度上提高了一个数量级,为O(N),但不仅仅是时间复杂度上的优化,该方法只需要IO数据文件一次,而算法1的IO次数较多的,因此该算法2比算法1在工程上有更好的可操作性。
第二步:找出Top 10
1、普通排序
时间复杂度是O(NlgN)。
题目要求是求出Top 10,因此我们没有必要对所有的Query都进行排序,我们只需要维护一个10个大小的数组,初始化放入10个Query,按照每个Query的统计次数由大到小排序,然后遍历这300万条记录,每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数据淘汰,加入当前的Query。最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top10了。
算法的最坏时间复杂度是O(N*K), 其中K是指top数。(每次淘汰一条数据后需要比较k次,移动k次)
在算法2中,我们已经将时间复杂度由O(NlogN)优化到O(NK)。
分析一下,在算法2中,每次比较完成之后,需要的操作复杂度都是K,因为要把元素插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组是有序的,我们每次查找的时候可以采用二分的方法查找,这样操作的复杂度就降到了logK,可是,随之而来的问题就是数据移动,因为移动数据次数增多了。不过,这个算法还是比算法2有了改进。
基于以上的分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构呢?回答是肯定的,那就是堆。
我们可以维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。时间复杂度由O(K)降到了O(logK)。
那么这样,采用堆数据结构,算法三,最终的时间复杂度就降到了O(N*logK),和算法2相比,又有了比较大的改进。
B. 海量日志数据,提取出某日访问百度次数最多的那个IP。
IP的数目还是有限的,最多2^32个,可以考虑使用映射的方法,比如%1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的IP及相应的频率。然后再在这1000个最大的IP中,找出那个频率最大的IP,即为所求。
(2)双层桶划分
事实上,与其说双层桶划分是一种数据结构,不如说它是一种算法设计思想。面对一堆大量的数据我们无法处理的时候,我们可以将其分成一个个小的单元,然后根据一定的策略来处理这些小单元,从而达到目的。
【适用范围】
第k大,中位数,不重复或重复的数字
【基本原理及要点】
因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。可以通过多次缩小,双层只是一个例子,分治才是其根本(只是“只分不治”)。
【扩展】
当有时候需要用一个小范围的数据来构造一个大数据,也是可以利用这种思想,相比之下不同的,只是其中的逆过程。
【问题实例】
A. 2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
方案1:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存2^32 * 2 bit=1 GB内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。
B. 5亿个整数中找出它们的中位数(正中间的数)。
首先将整数划分为2^16个区域,然后读取数据并统计落到各个区域里数的个数,之后我们根据统计结果就可以判断中位数落到哪个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。
这个题刚好和上面两个思想相反,一个0到3万的随机数生成器要生成一个0到35万的随机数。那么我们完全可以将0-35万的区间分成35/3=12个区间,然后每个区间的长度都小于等于3万,这样我们就可以用题目给的随机数生成器来生成了,然后再加上该区间的基数。那么要每个区间生成多少个随机数呢?计算公式就是:区间长度*随机数密度,在本题目中就是30000*(20000/350000)。最后要注意一点,该题目是有隐含条件的:彩票,这意味着你生成的随机数里面不能有重复,这也是为什么用双层桶划分思想的另外一个原因。
(3)Bloom Filter
【适用范围】
可以用来实现数据字典,进行数据的判重,或者集合求交集。
【基本原理及要点】
如果想判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表、树、Hash表等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O(n),O(log n),O(n/k)。
布隆过滤器的原理是,当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个位阵列(Bit array)中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检索元素一定不在;如果都是1,则被检索元素很可能在。这就是布隆过滤器的基本思想。
布隆过滤器存储空间和插入/查询时间都是常数O(k)。
【Bloom Filter的不足】
缺点是有一定的误识别率和删除困难。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。
误判是指某个元素并不存在于集合中,却判定为存在于集合中。
【问题实例】
给你A、B两个文件,各存放50亿条URL,每条URL占用64字节,内存限制是4G,让你找出A、B文件共同的URL。如果是三个乃至n个文件呢?
方案1:可以估计每个文件安的大小为5G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。
遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,...,a999)中。这样每个小文件的大约为300M。
遍历文件b,采取和a相同的方式将url分别存储到1000小文件(记为b0,b1,...,b999)。这样处理后,所有可能相同的url都在对应的小文件(a0 vs b0, a1 vs b1,..., a999 vs b999)中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。
求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。
方案2:果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。
(3)Bit-map
所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。
可进行数据的快速查找、判重、删除,一般来说数据范围是int的10倍以下。
【基本原理及要点】
使用bit数组来表示某些元素是否存在,比如8位电话号码。
【扩展】
Bloom filter可以看做是对bit-map的扩展。
【问题实例】
A. 已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话)。
申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。
(4)数据库索引
【适用范围】
大数据量的增删改查。
【基本原理及要点】
利用数据的设计实现方法,对海量数据的增删改查进行处理。
(4)倒排索引
【适用范围】
搜索引擎,关键字查询。
【基本原理及要点】
为何叫倒排索引?一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。
以英文为例,下面是要被索引的文本:
T0 = "it is what it is"
T1 = "what is it"
T2 = "it is a banana"
我们就能得到下面的反向文件索引:
"a": {2}
"banana": {2}
"is": {0, 1, 2}
"it": {0, 1, 2}
"what": {0, 1}
检索的条件"what","is"和"it"将对应集合的交集。
正向索引开发出来用来存储每个文档的单词的列表。正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文档中的验证这样的查询。在正向索引中,文档占据了中心的位置,每个文档指向了一个它所包含的索引项的序列。也就是说文档指向了它包含的那些单词,而反向索引则是单词指向了包含它的文档,很容易看到这个反向的关系。
(5)外排序
【适用范围】
大数据的排序,去重。
【基本原理及要点】
外排序的归并方法,置换选择败者树原理,最优归并树。
【问题实例】
有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16个字节,内存限制大小是1M。返回频数最高的100个词。
这个数据具有很明显的特点,词的大小为16个字节,但是内存只有1M做hash有些不够,所以可以用来排序。内存可以当输入缓冲区使用。
(5)分布式处理之Mapreduce
【什么是MapReduce】
MapReduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。这样做的好处是可以在任务被分解后,可以通过大量机器进行并行计算,减少整个操作的时间。但如果你要我再通俗点介绍,那么,说白了,Mapreduce的原理就是一个归并排序。
【适用范围】
数据量大,但是数据种类小可以放入内存。
【基本原理及要点】
将数据交给不同的机器去处理,数据划分,结果归约。
BTW:
public class HeapSort {
public static void main(String[] args) {
int numLength = 1000000000;
int bigLength = 1000;
int[] a = new int[bigLength];
Random r=new Random(10);
Random r2=new Random(10);
for(int i=0;i<bigLength;i++) {
a[i] = r.nextInt();
}
Sort(a);
Display(a, "before sort : ");
long start = System.currentTimeMillis();
for(int i=bigLength;i<numLength;i++) {
if(a[0] < r2.nextInt()) {
a[0] = r2.nextInt();
Sort(a);
}
}
Display(a, "After sort : ");
long end = System.currentTimeMillis();
System.out.println(end-start);
}
public static void Sort(int[] a) {
int n = a.length;
int temp = 0;
for (int i = n / 2; i> 0; i--)
Adjust(a, i - 1, n);
for (int i = n - 2; i >= 0; i--) {
temp = a[i + 1];
a[i + 1] = a[0];
a[0] = temp;
Adjust(a, 0, i + 1);
}
}
public static void Adjust(int[] a, int i, int n) {
int j = 0;
int temp = 0;
temp = a[i];
j = 2 * i + 1;
while (j <= n - 1) {
if (j < n - 1 && a[j] < a[j + 1])
j++;
if (temp >= a[j])
break;
a[(j - 1) / 2] = a[j];
j = 2 * j + 1;
}
a[(j - 1) / 2] = temp;
}
public static void Display(int[] a, String str) {
System.out.println(str);
for (int i = 0; i < a.length; i++)
System.out.print(a[i] + " ");
System.out.println();
}
}