1. 给定一个大小超过 100G 的文件, 其中存在 IP 地址, 找到其中出现次数最多的 IP 地址(hash文件切分)
该问题的一般解决思路是:首先将100G文件加载到内存以哈希表的形式将其储存起来,其中哈希表中的每个数组元素都是一个键值对。一个键值对,其中的Key值是IP地址(4字节),Value值则为IP地址出现的次数,再对哈希表遍历,查找出现次数最多的IP地址即可。但是一般的内存不可能有100G,所以存在无法将文件中的所有IP地址都保存起来的问题,所以上述的方法不可行。
因此我们要采取哈希文件划分的方法来处理。
假设现在有1G的内存,将这1G内存构造出一个哈希表,哈希表中的数组元素为一键值对Key-Value,其中Key用于存储IP地址,占4个字节,Value表示key对应的IP地址出现的次数,也用4字节来存放。因此1G的哈希表中最多可以存放2^27(1GB = 2^30B,2^30B/8B = 2^27)个数组元素:
(1)对100G文件进行第一次遍历读取IP地址。读取一个IP地址,根据哈希函数(一般%哈希表能表示的最大元素个数,以保证得到的存入位置在哈希表所表示的范围内)计算得到一个存入位置offset,我们可以使offset对100求模,将求模后的结果与0进行比较。
1)读取第一个IP地址ip1,并计算offset1。如果offset1 % 100 == 0,则将该ip1存放在哈希表中,下标由offset1表示(此处Key为ip1,Value为1)。如果求余后结果不为0,则跳过ip1,读取第二个IP地址。
2)读取第二个IP地址ip2,并计算offset
如果offset2 % 100 == 0;则将ip2存放在哈希表中,如果ip2等于ip1,则将ip1对应的Value值加1。否则,将ip2存放在下标为offset2表示的数组元素内,并填写相应的Key和Value。
如果求模运算不为0,则跳过ip2,读取下一个IP地址。
3)读取第三个IP地址重复2)的操作。
......
当读取完文件的最后一个IP地址,并对其作出相应处理之后。对这1G内存的哈希表的插入操作就结束了。此时哈希表中存放的都是满足:offset % 100 == 0的IP地址。然后对该哈希表进行查找,找到出现次数最多的IP地址(如IP0),将其IP地址和出现的次数保存起来,然后将哈希表清空。
(2)对100G文件中进行第二次遍历读取IP地址。读取一个IP地址,根据上述哈希函数计算得到一个哈希数offset,此时的哈希函数可以使offset在1G数组的下标所能表示的范围内。使offset对100求模,将求模后的结果与1进行比较。
1)读取第一个IP地址ip1,并计算offset1。如果offset1 % 100 == 1,则将该ip1存放在哈希表中,下标由offset1表示(此处Key为ip1,Value为1)。如果求模运算不为1,则跳过ip1,读取第二个IP地址。
2)读取第二个IP地址ip2,并计算offset2。
如果offset2 % 100 == 1;则将ip2存放在哈希表中,如果ip2等于ip1,则将ip1对应的value值加1。否则,将ip2存放在下标为offset2表示的数组元素内,并填写相应的key和value。
如果求模运算不为1,则跳过ip2,读取下一个IP地址。
3)读取第三个IP地址,重复2)的操作。
.......
当读取完文件的最后一个IP地址,并对其作出相应处理之后,对这1G内存的哈希表的插入操作就结束了。此时哈希表中存放的都是满足:offset % 100 == 1的IP地址。然后对该哈希表进行查找,找到出现次数最多的IP地址(如IP1),将其IP地址和出现的次数保存起来,然后将哈希表清空。
.....................
按照以上的方式对100G文件中一直进行遍历读取IP地址,直至进行到offset对100的取模运算。读取一个IP地址,根据上述哈希函数计算得到一个哈希数offset,此时的哈希函数可以使offset在1G数组的下标所能表示的范围内。使offset对100求模,将求模后的结果与99进行比较。
之后的每次取IP地址之后计算的offset值与100进行取模运算之后,再与99进行比较。如果模值等于99,则插入到哈希表中,否则不插入,遍历下一个IP值。直到遍历完文件中的所有IP地址。然后对哈希表进行查找,找到出现次数最多的IP地址如IP99,将IP99和出现的次数保存起来。
在上述中,对文件进行100次遍历。每次遍历将文件中的一部分IP地址保存在哈希表中进行处理,得到该部分的出现次数最多的IP地址。经100次遍历结束后,就可以保证对文件中的所有IP地址都进行处理过。最终得到100个出现次数较多的IP地址,将这100个IP地址的次数再次进行比较,得到出现最多的IP地址即为该100G文件中出现次数最多的IP地址。
在上述中,将100G文件划分100次,每次处理一部分,将该部分插入到哈希表中。100次之后,就将该文件的所有内容都处理过了。这种方法即为哈希文件划分法。
2. 给定100亿个整数, 找到其中只出现一次的整数(位图变形, 用两位来表示次数)
100亿个整数,一个整数占4个字节,总共需要400亿B。因为1GB = 10亿B,所以需要40GB的内存。此时需要的内存过大,因此不能直接将数据加载到内存中,因此需要进行处理。
题目要求找到只出现一次的整数,所以可以将出现两次和两次以上的整数归为一类进行处理。所以,对于任意的整数,可以分三个状态进行讨论:没有出现,出现一次,出现多次。因此三个状态可以两个比特位来存放。其中:00表示没有出现的整数,01表示出现1次的整数,10表示出现两次或者多次的整数,11则是舍弃不用。所以可以用位图来对这些数据进行处理,此时内存占用就可以减少为2.5GB。
所以,实现思路如下:
首先在内存中创建一个2.5GB的位图,初始化时全部设置为0。然后遍历100亿个整数,遇到一个整数,将该整数的2倍处的连续两个比特位先设置为01,当遇到两次或两次以上的情形时,将这两个比特位设置为10即可。遍历完所有整数后,对位图的设置就结束了。
然后,从头开始遍历位图,一次提取两个比特位,如果这两个比特位对应的数字为01,说明这两个比特位对应的数字只出现了一次,遍历完整个位图之后,就找到了所有只出现一次的数字。
如果给定的数字更多,创建的位图内存还是很大,此时还可以使用1中寻找IP地址用到的哈希划分的方法,给一个较小的内存,对所给的数据进行一部分一部分的处理。处理完一部分,得到一批只出现了一次的数字;待处理完所有部分之后,就得到了所有只出现一次的数字。
3. 有两个文件, 分别有100亿个query(查询词, 字符串), 只有1G内存, 找到两个文件的交集(hash文件切分 + 布隆过滤器)
因为两个文件过大,所以将两个文件均分为多个部分依次处理。比如可以将两个文件均分为10个部分,再创建两个布隆过滤器来存放两个文件其中一部分的字符串。
(1)处理文件1中的第一个部分:
遍历文件1中的字符串,遍历到一个,将该字符串通过字符串哈希函数计算得到一个hash值。将该hash对10求模。如果:hash % 10 == 0,此时将该字符串插入到布隆过滤器1中,否则继续遍历处理下一个字符串。当遍历完文件1之后,就将文件1中的第一部分插入到了布隆过滤器1中。
1)遍历文件2的字符串,利用上述相同的方法将文件2中字符串满足:hash % 10 == 0的一部分插入到布隆过滤器2中。此时处理完了文件2的一部分数据。然后分别遍历两个布隆过滤器找到相同的字符串集1保存起来。
2)再次遍历文件2的字符串,利用上述相同的方法将文件2中字符串满足:hash % 10 == 1的一部分插入到布隆过滤器2中。此时处理完了文件2的一部分数据。然后分别遍历两个布隆过滤器找到相同的字符串集2保存起来。
..........
10)第10次遍历文件2的字符串,利用上述相同的方法将文件2中字符串满足:hash % 10 == 9的一部分插入到布隆过滤器2中。此时处理完了文件2的一部分数据。然后分别遍历两个布隆过滤器找到相同的字符串集10保存起来。
总共遍历完10次文件2之后,便得到10个字符串集,这10个字符串集就是文件2中所有字符串与文件1中第一部分字符串的交集。
(2)处理文件1中的第二个部分:
遍历文件1中的字符串,遍历到一个,将该字符串通过字符串哈希函数计算得到一个hash值。将该hash对10求模。如果:hash % 10 == 1,此时将该字符串插入到布隆过滤器1中。否则继续遍历处理下一个字符串。当遍历完文件1之后,就将文件1中的第二部分插入到了布隆过滤器1中。
再次进行上述的1)~10)操作。再次遍历完10次文件2之后,又得到10个字符串集,这10个字符串集就是文件2中所有字符串与文件1中第二部分字符串的交集。
.............
(10)处理文件1的第10个部分:
遍历文件1中的字符串,如果:hash % 10 == 9,将该字符串插入到布隆过滤器1中,否则遍历下一个字符串。当遍历完整个文件1之后,就将文件1的第10部分插入到了布隆过滤器1中。
再次进行上述的1)~10)操作。再次遍历完10次文件2之后,又得到10个字符串集,这10个字符串集就是文件2中所有字符串与文件1中第十部分字符串的交集。
此时,便可以得到10*10=100个字符串集。这100个字符串集就是文件1与文件2的交集。
4. 给上千个文件, 每个文件大小为1K - 100M, 设计算法找到某个词存在在哪些文件中(倒排索引)
可以创建一个哈希表,每个元素是一个键值对。其中key表示的是文件中出现的词,value表示词语出现的文件编号。
分别遍历各文件,遇到一个没有出现过的词语将它插入到哈希表中。此时key即为该词语,value设置为该文件的编号。遇到相同的词语时,如果该词语(key)对应的文件编号已经保存在value中。则继续遍历下一个词语;如果该词语对应的文件编号还没保存在value中,则将该文件编号添加进value中。当遍历完所有文件后,就将所有词语以及它所对应的文件编号保存在了哈希表中。
当给出某个要查找的词时,在哈希表对key值进行查找,如果有对应的key值,则该key值对应的value值即为该词语出现的文件编号。如果不存在该key值与该词语对应,则说明所有文件中都没有该词语。
比如:(此时为正排索引)
文件1:hello world haha
文件2:hehe haha china
文件3:china world
在存放时:(此时为倒排索引)
key value
hello 1
world 1 2
haha 1 2
hehe 2
china 2 3
如果此时要查找haha,对应的文件编号即为1,2。