一、字典树/Trie树:
1.是一种用于快速检索的多叉树结构。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
优点:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
2.三个基本特性:
1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3)每个节点的所有子节点包含的字符都不相同。
3.Trie树的应用:
1)字符串检索,词频统计,搜索引擎的热门查询
事先将已知的一些字符串(字典)的有关信息保存到Trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
1)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
解答:分而治之+hash统计+堆/快排/归并
S1:分而治之/hash映射:顺序读取文件,对于每个词x,取hash(x)%5000,然后按照该值存到5000个小文件中(记为x0,x1,...x4999),则每个小文件的大小大约为200K,若某个文件大于1M,则按照类似方法继续分解,直到文件小于1M。
S2:hash_map统计:对于每个小文件,采用Trie树或者hash_map等统计每个文件中出现的词及出现的频率。
S3:归并排序:在每个小文件(x0,x1,...x4999)中,取出出现频率最大的100个词(可以用含100个结点的最小堆)后,再把100个词存到小文件中,这样又得到5000个小文件。最后就是对这5000个小文件进行归并排序了。
解答二、针对此类典型的TOP K问题,采取的对策往往是:hashmap + 堆。
S1:hash_map统计:先对这批海量数据预处理。具体方法是:维护一个Key为Query字串,Value为该Query出现次数的HashTable,即hash_map(Query,Value),每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内用Hash表完成了统计;
S2:堆排序:借助堆这个数据结构,找出Top K,时间复杂度为N*logK。即借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。所以,我们最终的时间复杂度是:O(N) + N' * O(logK),(N为1000万,N’为300万)。
别忘了这篇文章中所述的堆排序思路:“维护k个元素的最小堆,即用容量为k的最小堆存储最先遍历到的k个数,并假设它们即是最大的k个数,建堆费时O(k),并调整堆(费时O(logk))后,有k1>k2>...kmin(kmin设为小顶堆中最小元素)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,若x>kmin,则更新堆(x入堆,用时logk),否则不更新堆。这样下来,总费时O(k*logk+(n-k)*logk)=O(n*logk)。此方法得益于在堆中,查找等各项操作时间复杂度均为logk。”。
2)给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
3)给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。
4)1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串
5)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
解答:虽然有一千万个查询,但是重复率较高,事实上只有300万个查询,每个查询255B,共需内存3*10^6*1/4K=0.75G,可以把他们都放进内存中去。
方案一:采用Trie树,关键字域存该查询串出现的次数,没有出现的为0。最后用10个元素的最小堆对出现频率进行排序。算法的时间复杂度为O(n*len),其中,n为查询的个数,len为每个字符串的平均长度。
方案二:因300万查询可直接放入内存,所以我们放弃分而治之/hash映射的步骤,直接上hash统计,然后排序。So,针对此类典型的TOP K问题,采取的对策往往是:hashmap + 堆。
S1:hash_map统计:先对这批海量数据预处理。具体方法是:维护一个Key为Query字串,Value为该Query出现次数的HashTable,即hash_map(Query,Value),每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内用Hash表完成了统计。
S2:堆排序:借助堆这个数据结构,找出Top K,时间复杂度为N’*logK。即借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。所以,我们最终的时间复杂度是:O(N) + N' * O(logK),(N为1000万,N’为300万)。
二、哈希表
哈希表也称散列表,hashtable(key,value) 的做法:把Key通过一个固定的算法函数即所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组里。可以把输入范围很广的关键字压缩到固定长度的输出中。
这样的话,相同输入会被放到同一个文件中,不同输入放到不同文件中。
而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。
适用范围:快速存取、统计某些数据,将大量数据进行分类。
基本原理及要点:hash函数选择,针对字符串,整数,排列,具体相应的hash方法。
解决冲突方法:一种是开放地址法,另一种是链地址法。(哈希表空间为[0,m-1],设置一个由m个指针组成的数组ch[m],把所有哈希地址为i的数据元素都插入到头指针为ch[i]的链表中)
举例:
1)海量日志数据,提取出某日访问百度次数最多的那个IP。
IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。
解答:分而治之+Hash统计+快排/堆
S1:hash映射:IP地址最多有2^32=4G种取值情况,所以不能完全加载到内存中处理。可以考虑采用“分而治之”的思想,按照IP地址的Hash(ip)%1024值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址。
S2:hash_map统计:对于每一个小文件,可以构建一个IP为key,出现次数为value的hash_map,同时记录当前出现次数最多的那个IP地址。
S3:可以得到1024个小文件的出现次数最多的IP,再利用堆/快排,得到出现次数最多的IP。
关于本题,还有几个问题,如下:
1、Hash取模是一种等价映射,不会存在同一个元素分散到不同小文件中的情况,即这里采用的是mod1000算法,那么相同的IP在hash取模后,只可能落在同一个文件中,不可能被分散的。因为如果两个IP相等,那么经过Hash(IP)之后的哈希值是相同的,将此哈希值取模(如模1000),必定仍然相等。
2、那到底什么是hash映射呢?简单来说,就是为了便于计算机在有限的内存中处理big数据,从而通过一种映射散列的方式让数据均匀分布在对应的内存位置(如大数据通过取余的方式映射成小树存放在内存中,或大文件映射成多个小文件),而这个映射散列方式便是我们通常所说的hash函数,设计的好的hash函数能让数据均匀分布而减少冲突。尽管数据映射到了另外一些不同的位置,但数据还是原来的数据,只是代替和表示这些原始数据的形式发生了变化而已。
三、位图法
位图法的做法:第一次扫描集合,找出最大元素max。按照集合中最大元素max创建一个长度为max+1的新数组,初始值为0。然后再次扫描原数组,遇到M就给新数组的第M+1位置上1。如遇到5就给新数组的第六个元素置1,这样下次再遇到5想置位时发现新数组的第六个元素已经是1了,这说明这次的数据肯定和以前的数据存在着重复。它的运算次数最坏的情况为2N。如果已知数组的最大值即能事先给新数组定长的话效率还能提高一倍。
适用范围:可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下。
举例:
1)已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。
2)2.5亿个整数中找出不重复整数的个数,内存空间不足以容纳这2.5亿个整数。
解答:方案一:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存2^32 * 2 bit=1 GB内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。
方案二:也可采用与第1题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。
3)给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
解答:可以估计每个文件安的大小为5G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。
S1:遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,...,a999)中。这样每个小文件的大约为300M。
遍历文件b,采取和a相同的方式将url分别存储到1000小文件(记为b0,b1,...,b999)。这样处理后,所有可能相同的url都在对应的小文件(a0vsb0,a1vsb1,...,a999vsb999)中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。
S2:求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。
4)腾讯面试题:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
解答:方案一:申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。(2^32bite=4G,512M=2^32bite/2^3=2^29B=512M)
方案二:这个问题在《编程珠玑》里有很好的描述,大家可以参考下面的思路,探讨一下:
又因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;
这里我们把40亿个数中的每一个用32位的二进制来表示,
假设这40亿个数开始放在一个文件中。
然后将这40亿个数分成两类:
1.最高位为0
2.最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);
与要查找的数的最高位比较并接着进入相应的文件再查找;
再然后把这个文件为又分成两类:
1.次最高位为0
2.次最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了);
与要查找的数的次最高位比较并接着进入相应的文件再查找。
.......
以此类推,就可以找到了,而且时间复杂度为O(logn),方案2完。
例题:1、有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。
解答:
方法一:S1:hash映射。顺序读取10个文件,按照hash(query)%10的结果,将query输出到另外10个文件中(a1,a2,...a10)。假设hash函数是随机的,这样新生成的文件大小约为1G。
S2:hash_map统计:找一台内存为2G的机器,依次用hash_map(query,query_count)统计每个查询出现的次数。
S3:堆/快排/归并:利用堆/快排/归并按照查询出现的次数进行排序。将排序后的query和query_count输出到文件中,这样得到10个排好序的文件(b0,b1,...b9)。最后,对这10个文件进行归并排序(内排和外排相结合)。
方案二:一般来说,查询总量是有限的,只是重复的次数比较多而已。可能对于所有的query,可以一次性加入到内存。这样可以利用Trie树直接统计每个query出现的次数。然后按照出现的次数,做堆排序/快排/归并。
方案三:与方案一类似,但做完hash映射,分成多个文件之后,可以交给多个文件来处理,采用分布式的架构来处理(比如MapReduce),最后再进行合并。
5)一个文件中有9亿条不重复的9位整数,对这个文件中的数字进行排序。
解答:
32位机器中,9亿*4B=36亿B=3.6G,数据无法一次性加入到内存。
方案一:分治法
S1:hash映射:将9亿条数据分成20段,每段大约5000万条,占用内存5*10^7*4B=200MB。在文件中依次搜索0~5000万,5000万~1亿。。。将排序后的结果放入到文件。
方案二:位图法
最大是整数为999 999 999,由于9亿条数据不重复,可以将这些数据组成队列或数组,数组下标从0到999 999 999,数组的值对应该数值是否存在。0表示不存在,1表示存在。判断0和1只有一个bit存储即可。共需内存10亿/8=120M。初始化数组为0,读取文件中的数据,若读到的数m,则将数组下标为m的元素置1.遍历整个bit数组,将bit为1 的数组下标存入文件,最终得到排序后的内容。
此第一种方法:分而治之/hash映射 + hash统计 + 堆/快速/归并排序,再看最后几道题,如下:
1、怎么在海量数据中找出重复次数最多的一个?
方案:先做hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。然后找出上一步求出的数据中重复次数最多的一个就是所求(具体参考前面的题)。
2、上千万或上亿数据(有重复),统计其中出现次数最多的前N个数据。
方案:上千万或上亿的数据,现在的机器的内存应该能存下。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后利用堆取出前N个出现次数最多的数据。
3、一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
方案1:如果文件比较大,无法一次性读入内存,可以采用hash取模的方法,将大文件分解为多个小文件,对于单个小文件利用hash_map统计出每个小文件中10个最常出现的词,然后再进行归并处理,找出最终的10个最常出现的词。
方案2:通过hash取模将大文件分解为多个小文件后,除了可以用hash_map统计出每个小文件中10个最常出现的词,也可以用trie树统计每个词出现的次数,时间复杂度是O(n*le)(le表示单词的平准长度),最终同样找出出现最频繁的前10个词(可用堆来实现),时间复杂度是O(n*lg10)。
4、1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
方案:这题用trie树比较合适,hash_map也行。
5、一个文本文件,找出前10个经常出现的词,但这次文件比较长,说是上亿行或十亿行,总之无法一次读入内存,问最优解。
方案1:首先根据用hash并求模,将文件分解为多个小文件,对于单个文件利用上题的方法求出每个文件件中10个最常出现的词。然后再进行归并处理,找出最终的10个最常出现的词。
6、100w个数中找出最大的100个数。
方案1:采用快速排序的思想,每次分割之后只考虑比轴大的一部分,知道比轴大的一部分在比100多的时候,采用传统排序算法排序,取前100个。复杂度为O(100w*100)。
方案2:在前面的题中,我们已经提到了,用一个含100个元素的最小堆完成。复杂度为O(100w*lg100)。
四、Bloom Filter:
基本原理及要点:
对于原理来说很简单:位数组+k个独立hash函数。将hash函数对应的值的位数组置1。查找时如果发现所有hash函数对应位都是1说明存在,很明显这个过程并不保证查找的结果是100%正确的。同时也不支持删除一个已经插入的关键字,因为该关键字对应的位会牵动到其他的关键字。所以一个简单的改进就是 counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。
还有一个比较重要的问题,如何根据输入元素个数n,确定位数组m的大小及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的。所以使用bloom filter内存上通常都是节省的。
问题实例:给你A,B两个文件,各存放50亿条URL,每条URL占用64字节,内存限制是4G,让你找出A,B文件共同的URL。如果是三个乃至n个文件呢?根据这个问题我们来计算下内存的占用,4G=2^32大概是40亿*8大概是340亿,n=50亿,如果按出错率0.01算需要的大概是650亿个bit。现在可用的是340亿,相差并不多,这样可能会使出错率上升些。另外如果这些urlip是一一对应的,就可以转换成ip,则大大简单了。
如果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。”
五、外排序法
适用范围:大数据的排序,去重。
基本原理及要点:外排序的归并方法,置换选择败者树原理,最优归并树。
实现步骤:
S1:文件预处理:把含有n个记录的文件,按内存大小划分为若干个长度为L的子文件,然后分别把子文件调入内存,采用有效的内排序之后,送回外存。
S2:多路归并:把这些初始归并段进行多遍归并,使得有序归并段逐渐扩大。最后在外存上形成整个文件的单一归并段。
举例:
1).有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16个字节,内存限制大小是1M。返回频数最高的100个词。
这个数据具有很明显的特点,词的大小为16个字节,但是内存只有1m做hash有些不够,所以可以用来排序。内存可以当输入缓冲区使用。
六、Mapreduce
适用范围:数据量大,但是数据种类小可以放入内存,进行并行计算。
基本原理及要点:将数据交给不同的机器去处理,数据划分,结果归约。
实现步骤:S1:Map(映射),先通过map程序把一组键值映射成一组新的键值,将数据切割成不相关的区块,分配给处理,达到分布计算的结果。
S2:通过Reduce(化简),通过并发的reduce函数将结果汇总,保证所有映射键值对中的每一个,共享相同键值。
扩展:
问题实例:
1).The canonical example application of MapReduce is a process to count the appearances of each different word in a set of documents:
2).海量数据分布在100台电脑中,想个办法高效统计出这批数据的TOP10。
3).一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到N^2个数的中数(median)?
七、双层桶--其实本质上就是【分而治之】的思想,重在“分”的技巧上!
适用范围:第k大,中位数,不重复或重复的数字
基本原理及要点:因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。可以通过多次缩小,双层只是一个例子。
扩展:
问题实例:
1).2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
有点像鸽巢原理,整数个数为2^32,也就是,我们可以将这2^32个数,划分为2^8个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区域,然后不同的区域在利用bitmap就可以直接解决了。也就是说只要有足够的磁盘空间,就可以很方便的解决。
2).5亿个int找它们的中位数。
这个例子比上面那个更明显。首先我们将int划分为2^16个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。
实际上,如果不是int是int64,我们可以经过3次这样的划分即可降低到可以接受的程度。即可以先将int64分成2^24个区域,然后确定区域的第几大数,在将该区域分成2^20个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有2^20,就可以直接利用direct addr table进行统计了。