**大数据处理面试题分析**
最近学习了关于搜索方面的数据结构--搜索树,AVL树,红黑树,哈希表,哈希表的扩展-位图,布隆过滤器;
大数据在当前社会下是非常火的,同样随之而来的是在IT行业进行面试的时候,也就变成了经常问的话题;
所以我就尝试利用所学知识对以下大数据方面的面试题进行分析:
我发现,如果是内存放不下,并且位图等数据结构也用不上的情况下,哈希切分则扮演重要的角色!
当然,我建议如果你对数据结构有了一定的了解之后再看这篇文章比较好,否则可能看了也白看!
譬如说,你不知道什么是堆,哈希表,位图,那就可以不看了,哈哈!
我们学习这些数据结构的目的就在于知道用它的实际应用,应该知其所以然!
(个人所学和理解程度有限,希望读者可以指出不足,共同学习!)
1)给⼀一个超过100G⼤小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?!
题目分析:
首先,我们看到的是一个大小为100G的一个日志文件,第一点就应该想到的是一般计算机的内存放不下吧; 肯定需要切分,切成100份,把这100个文件当作是大小为100的哈希表,每份只有1G的大小,就可以读入内存进行处理,内存中该怎么处理?往下看;
然后,再看题目要求:找出出现次数最多的IP;
那么,内存中怎样处理的依据就出来了,我们当然需要把相同的IP放入同一个文件中,因为要统计出现次数;
而怎样做到把相同的IP放入到这100个小文件中的同一个文件呢?
哈希函数!
我们在学习哈希表的时候,知道关键字key映射相应的存储位置,其中对于字符串有相应的哈希函数将字符串转为对应的key,那么我们把IP当作字符串做同样的处理,得到key,然后根据index = key%100决定存在哪一个文件中,而相同的IP得到的key肯定相同,也就进入了同一个文件;
类似于下面这个函数得到key:
size_t HashFunc1(const K& key)
{
const char* str = key.c_str();
unsigned int seed = 131;
unsigned int hash = 0;
while(*str)
{
hash = hash*seed + (*str++);
}
return(hash& 0x7FFFFFFF);
};
上面的这些步骤才是重点(就是哈希切分的过程),下面的就是常规过程了;
现在我们要重新依次把这100个文件读入内存,每读入内存一个文件,我们要看的事情是 统计这个文件中出现次数最多的IP(快一点方法,利用key就是IP,value代表出现次数的key_value的结构的搜索树就可以),用MAX记录下来,
再读第二个文件,统计出新文件中出现次数最多的IP,和MAX比较,如果大于MAX就更新MAX,继续上述步骤!
这就是基本的算法了;本题的重点是哈西切分!
2)与上题条件相同,如何找到top K的IP?如何直接⽤用Linux系统命令实现?!
题目分析:
本题也是进行哈希切分,这个过程我就省略了;
第一题只要找最多的一个,而这道题要找最多的K个;其实我们学过数据结构的话,应该一看到top K就应该
想到 堆排序,那么问题其实就简单了,当然,我们还是得统计第一个文件中每个IP出现的次数(方法同第一题),
如果是一棵搜索树的话,取其中的K个(key_value结构)结点建一个最小堆;
注意:这里是建最小堆!!! 我们一看到top K就下意识的想回答建最大堆,那就中计了;
为什么是建最小堆?
因为当前这堆中的K个IP不一定就是top K,所以我们还得往堆里插入;而又必须保证堆的大小还是K,而且堆中的K个结点的IP还是当前出现次数最多的K个,所以我们得和堆中的结点互换,那么,重点是该用哪个结点和新插入的结点交换?
最符合条件的肯定是堆顶元素,前提是最小堆,那么堆顶元素就是当前K个IP出现次数最小的,而我新来的这个IP出现的次数比堆顶的大,那么我就可以交换了,交换之后对堆进行一次向下调整,保证堆顶元素仍是最小!直到第一个结点构建的搜索树都比较完,然后,再读入第二个文件,构建搜索树,统计出现次数,和堆顶元素的比较,重复以上过程,直到结束!
最后堆中的K个IP就是top K;
3)给定100亿个整数,设计算法找到只出现一次的整数!
题目分析:
这道题就简单了,但是看到同样一个问题是100亿个整数,大约是35G的大小,但是整数能表示的最大范围也就是2的32次方
那么大约就是16G的大小,那么剩下的就都是重复的数,这道题没有规定死内存大小,但是16G还是太大了,如何继续缩小
需要的内存呢?
<位图>
我们可以利用位图的特性,位图的一般是一位表示一个key,这里我们需要两个比特位表示一个整数,因为我们需要表示的状态有:00不存在, 01出现一次,10出现多次(>=2次),11不用,那么我们大约需要1G的内存就可以了;
最后遍历找出出现一次的整数即可!
4)给两个⽂文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集!
题目分析:
又是100亿个整数,但是是要找两个文件的交集,这个用上面位图的方法就不太好做了,但是我们用数据结构处理不了的话,不是还有哈希切分嘛!
我们将两个文件分别切分为1000个文件,每个文件的大小就几十兆的样子,先对文件A分的1000个文件里的整数进行哈希分配,即取出来整数模除1000,使相同的整出进入相同的文件,文件B切分的1000个文件进行同样的处理,然后分别拿A哈希切分好的第一个文件和B哈希切分好的第一个文件对比,找出交集存到一个新文件中,依次类推,直到2000个文件互相比较完!
5)1个⽂文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数!
题目分析:
类似于第3题,利用位图求解!只不过11代表的是出现次数超过了两次的整数!
6)给两个⽂文件,分别有100亿个query,我们只有1G内存,如何找到两个⽂文件交集?分别给出精确算法和近似算法!
题目分析:
先看题目就和第四题很像,但是不同的是分别要近似算法和精确算法;
精确算法,就是第四题的解法;
近似算法:布隆过滤器
即利用哈希算法和位图实现;
先对A和B文件进行哈希切分,然后取A文件的第一个切分文件进行布隆过滤器的插入,然后取B文件的第一个切分文件进行布隆过滤器的查找,即判断是否存在!
需要注意的是为了减少误判,我们一般用多个哈希函数生成多个对应位,即一个字符串对应多个比特位;
为什么布隆过滤器是近似算法,是因为它的不存在是确定的,存在是不确定的,即一个字符串对应5个位, 如果有一个位为0,则这个字符串肯定不存在,如果一个字符串对应的5个位都为1,但是这个字符串却不
一定存在,因为可能这5个位都是被其它字符串的对应位置为1的!
大概实现代码如下,代码只是核心算法部分:
template
class BloomFilter
{
size_t HashFunc1(const K& key)
{
const char* str = key.c_str();
unsigned int seed = 131;
unsigned int hash = 0;
while(*str)
{
hash = hash*seed + (*str++);
}
return(hash& 0x7FFFFFFF);
};
size_t HashFunc2(const K& key)
{
const char* str = key.c_str();
register size_t hash = 0;
while (size_t ch = (size_t)*str++)
{
hash = 65599 * hash + ch;
//hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;
}
return hash;
}
size_t HashFunc3(const K& key)
{
const char* str = key.c_str();
register size_t hash = 0;
size_t magic = 63689;
while (size_t ch = (size_t)*str++)
{
hash = hash * magic + ch;
magic *= 378551;
}
return hash;
}
size_t HashFunc4(const K& key)
{
const char* str = key.c_str();
register size_t hash = 0;
size_t ch;
for (long i = 0; ch = (size_t)*str++; i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
size_t HashFunc5(const K& key)
{
const char* str = key.c_str();
if(!*str)
return 0;
register size_t hash = 1315423911;
while (size_t ch = (size_t)*str++)
{
hash ^= ((hash << 5) + ch + (hash >> 2));
}
return hash;
}
public:
BloomFilter(const size_t size)
:_size(size)
,_bitmap(size)
{}
void Set(const K& key)
{
size_t hash1 = HashFunc1(key);
_bitmap.Set(hash1%_size);
size_t hash2 = HashFunc1(key);
_bitmap.Set(hash2%_size);
size_t hash3 = HashFunc1(key);
_bitmap.Set(hash3%_size);
size_t hash4 = HashFunc1(key);
_bitmap.Set(hash4%_size);
size_t hash5 = HashFunc1(key);
_bitmap.Set(hash5%_size);
}
void ReSet()
{}
bool Test(const K& key)
{
size_t hash1 = HashFunc1(key);
if(!_bitmap.Test(hash1%_size))
return false;
size_t hash2 = HashFunc1(key);
if(!_bitmap.Test(hash2%_size))
return false;
size_t hash3 = HashFunc1(key);
if(!_bitmap.Test(hash3%_size))
return false;
size_t hash4 = HashFunc1(key);
if(!_bitmap.Test(hash4%_size))
return false;
size_t hash5 = HashFunc1(key);
if(!_bitmap.Test(hash5%_size))
return false;
return true;
}
private:
BitMap _bitmap;
size_t _size;
};
int main()
{
BloomFilter<> bloom(32);
bloom.Set("aaaaaaaaaa");
bloom.Set("bbbbbbbbbb");
bloom.Set("cccccccccc");
bloom.Set("dddddddddd");
cout<"aaaaaaaaaa")<"aaaaa")<"bbbbbbbbbb")<"bbbbb")<"dddddddddd")<"dddd")<"pause");
return 0;
}
7)如何扩展BloomFilter使得它支持删除元素的操作?!
算法分析:
因为布隆过滤器的一个Key对应多个位,所以如果要删除的话,就会有些麻烦,不能单纯的将对应位 全部置为0,因为可能还有其它key对应这些位,所以,需要对每一个位进行引用计数,以实现删除的 操作,那么也就引入了下面这个问题;
8)如何扩展BloomFilter使得它支持计数操作?!
题目分析:
作为上一道题的扩展,我们就要考虑引用计数该怎么实现的问题了,因为需要每一个对应位都需要一个计数,所以每一位至少需要一个int,那么我们就不得不放弃位图了,也就是放弃了最小的空间消耗,我们需要直接以一个就像数组一样的实现,只不过数组的内容存放的是引用计数;
大概实现代码如下:
template
class BloomFilter
{
size_t HashFunc1(const K& key)
{
const char* str = key.c_str();
unsigned int seed = 131;
unsigned int hash = 0;
while(*str)
{
hash = hash*seed + (*str++);
}
return(hash& 0x7FFFFFFF);
};
size_t HashFunc2(const K& key)
{
const char* str = key.c_str();
register size_t hash = 0;
while (size_t ch = (size_t)*str++)
{
hash = 65599 * hash + ch;
//hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;
}
return hash;
}
size_t HashFunc3(const K& key)
{
const char* str = key.c_str();
register size_t hash = 0;
size_t magic = 63689;
while (size_t ch = (size_t)*str++)
{
hash = hash * magic + ch;
magic *= 378551;
}
return hash;
}
size_t HashFunc4(const K& key)
{
const char* str = key.c_str();
register size_t hash = 0;
size_t ch;
for (long i = 0; ch = (size_t)*str++; i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
size_t HashFunc5(const K& key)
{
const char* str = key.c_str();
if(!*str)
return 0;
register size_t hash = 1315423911;
while (size_t ch = (size_t)*str++)
{
hash ^= ((hash << 5) + ch + (hash >> 2));
}
return hash;
}
public:
BloomFilter(const size_t size)
{
_map.resize(size);
for(size_t i = 0; i<_map.size();++i)
_map[i] = 0;
}
void Set(const K& key)
{
size_t hash1 = HashFunc1(key);
size_t hash2 = HashFunc2(key);
size_t hash3 = HashFunc3(key);
size_t hash4 = HashFunc4(key);
size_t hash5 = HashFunc5(key);
_map[hash1%_map.size()]++;
_map[hash2%_map.size()]++;
_map[hash3%_map.size()]++;
_map[hash4%_map.size()]++;
_map[hash5%_map.size()]++;
}
bool ReSet(const K& key)
{
size_t hash1 = HashFunc1(key);
if(_map[hash1%_map.size()]==0)
return false;
size_t hash2 = HashFunc2(key);
if(_map[hash2%_map.size()]==0)
return false;
size_t hash3 = HashFunc3(key);
if(_map[hash3%_map.size()]==0)
return false;
size_t hash4 = HashFunc4(key);
if(_map[hash4%_map.size()]==0)
return false;
size_t hash5 = HashFunc5(key);
if(_map[hash5%_map.size()]==0)
return false;
_map[hash1%_map.size()]--;
_map[hash2%_map.size()]--;
_map[hash3%_map.size()]--;
_map[hash4%_map.size()]--;
_map[hash5%_map.size()]--;
return true;
}
bool Test(const K& key)
{
size_t hash1 = HashFunc1(key);
if(_map[hash1%_map.size()] == 0)
return false;
size_t hash2 = HashFunc2(key);
if(_map[hash2%_map.size()] == 0)
return false;
size_t hash3 = HashFunc3(key);
if(_map[hash3%_map.size()] == 0)
return false;
size_t hash4 = HashFunc4(key);
if(_map[hash4%_map.size()] == 0)
return false;
size_t hash5 = HashFunc5(key);
if(_map[hash5%_map.size()] == 0)
return false;
return true;
}
private:
vector _map;
};
“`