一个金山的笔试题:
有一个日志文件,每行记录了一次调用信息,其中包括时间和来源IP。每天的记录数目大约10亿条左右。现在需要:
1)获取日访问次数最高的1000个来源IP,按照访问量从高到低排序。
2)获取连续一周内访问次数最高的1000个来源IP,按照访问量从高到低排序。
请给出能得到精确(非近似)结果,并且效率尽可能高的计算方法,并给出主要部分伪代码。
大数据量的问题,面临以下几个问题:
一、该10亿条数据中最多有多少条IP记录。考虑是否可以一次将记录读入内存,进行排序
二、IP记录过大,无法完全读入内存,则考虑外部排序。
三、考虑算法的优化,效率问题
四、根据实际的问题,分析考虑如何实现该算法。
五、是否可已导入数据库,使用数据库操作来实现(其实大部分公司,对日志的分析,都是导入的数据库中进行,并优化数据库操作,减低数据库压力)
l 不考虑内存空间是否能承受该10亿天记录
Ø 堆实现
用堆的方式保存源地址信息,每个节点的结构为
typedef unsigned long U32;
struct NODE
{
U32 ip; //源IP地址
U32 visitToday; //本日访问次数
U32 visitThisWeek; //本周访问次数
};
伪码:
U32 ip;
TIME time;
//以ip为key建立堆
ret = ReadLine(&ip, &time);
while (ret != EOF)
{
struct NODE *q = binarySearchWithIp(ip);
if (NULL == q)
{
q = GetHeapTailNode();
q->ip = ip;
q->visitToday = 0;
q->visitThisWeek = 0;
AdjustHeapWithIp();
}
if (IsToday(time)) (p->visitToday)++;
is (IsThisWeek(time)) (p->visitThisWeek)++;
ret = ReadLine(&ip, &time);
}
//以visitToday为Key对堆进行重新排序,只要排出前1000个即可
SortHeapTopNWithVisitToday(1000);
PrintHeapTopN(1000);
//以visitThisWeek为Key对堆进行重新排序,只要排出1000个即可
SortHeapTopNWithVisitThisWeek(1000);
PrintHeapTopN(1000);
l 外部存储的散列
我觉得要得出精确的结果,这个问题要分解成两个问题
1.统计ip访问量日访问量
2.进行排序
问题1,确实无法在内存里完成统计,需要借助外部存储,我暂时能想到的高效的文本文件结构式是根据ip进行散列,例如127.0.0.1,将散列到文件/127/0/0/ip.dat这个文件中,ip.dat中将存储255个记录,记录,均按照24位二进制存放,24位中头8位为一个整形用于存放ip最后一段,后16为为长整形用于存放访问次数,ip.dat 在创建的时候将直接填满 24*255的长度,根据ip的最后一段进行文件中的位置定位,这样就能够知道这条记录在ip.dat中的文位置,便于改写(例如127.0.0.1,1就在这个文件中的首位,127.0.0.129就在 129*24的位置上)。在统计的过程中,要注意一次要统计一定的记录数量后,在写入磁盘,这样可以降低磁盘读写次数,例如,做一个位图,key为ip,value为次数,遍历日志文件,当这个位图中的 key超过了10万条则写入磁盘。
问题2,实际上这个问题是如何收集1000条值最高的记录,而不应该是 对 所有的记录进行全部的排序,这样的话,可以通过一个大根堆,来进行过滤,每当堆内的节点达到1001的时候则将最小的remove掉,这样就将大数据量的排序的问题转化为小数据量排序,在遍历了一遍step1中的统计结果后,就能够得出准确的结果了
l 算法设计与分析方面
分成2个步骤,第一个步骤是分类统计
如果内存足够,就使用数组,2的32次方大小的UINT数组,也就是16GB。
如果内存不够,又不会出现太坏的情况,就用平衡树。(由于内存分配的原因,但如果出现最坏或者很坏的情况,比如10亿个IP中有1亿个以上不同的IP,平衡树实际所需要的内存反而比数组要大)
第二个步骤,就是在第一个步骤的基础上进行排序,找出最多的1000个IP了
最容易想到的就是快速排序,时间复杂度是 O(n*log2n)。堆排序的效率并不比快速排序高。
但这里的题目并不是要求我们对所有的IP进行排序,只是求出前1000个IP。所以,对于第二步,有另外一种思路,可以更高效的获得结果。
这里做一个假设,最后10亿个IP中有1亿个不同的IP。需要在1亿个IP中找到1000个IP。
对于快速排序来说复杂度就是: N*log2N(N=1亿)。大概等于26亿。
这里不直接使用快速排序,而是分为三个步骤。
步骤2.1:
IP1,IP2......IP100000000.现在IP1与IP2比较,IP3与IP4比较,IP99999999与IP100000000比较,得到较大的IP。
假设是IP2,IP4,IP5,IP8.......IP99999999.继续2个2个的比较,得到较大的IP。
假设是IP2,IP8......IP99999999。
这样经过M次比较(这里M=13),最后得到了大概1万个左右的IP。
步骤2.1复杂度是固定的,并且小于N(N=1亿)
步骤2.2:
这个时候对这1万个左右的IP进行快速排序,复杂度是 X*log2X(X=1万),找出前1000个IP。
步骤2.3:
那么剩下1000个IP,对于那大概9000个左右的IP,以及被这9000个IP淘汰掉的IP,都可以扔掉了。
我们只需要考虑1000个IP以及和被他们淘汰的IP,也就是1000*2的M次方(这里M=13),总共大概是800万个IP。
这个时候对800万个IP进行快速排序,复杂度是 Y*log2Y(Y=800万)
最终的时间复杂度是步骤2.1,2.2,2.3的总和,这里得到的结果大概是:1亿(步骤1)+ 13万(步骤2) + 1亿8千万(步骤3),大概是3亿不到。
补充:对于步骤2,我这里M=13只是随意给出的一个方案,对于不同的情况,M都不一样,可以通过复杂的计算得到M的最优值。另外,对于步骤2.3,实际上也可以递归分解为和步骤2一样的3个步骤。
数据量这么大,无论是用什么工具去处理都是不太好处理的.
我想法:
1) 它是按时间来进行统计的.所以,第一步,我觉得需要把这个十几亿或者是过百亿行的文件拆分了多个文件.可以按每小时或者每半小时为一个文件
2) 当拆分了多个文件后,每个文件中的IP进行统计,并且记录在持久性介质中
3) 重持久性介质中,再通过 Hash 等方式来进行排序
总结: 到了10亿这个层面上,常用算法都是变得相当无力.
我思想是现实有限硬件条件下,可以对大数据量处理进行拆分,统计,再重组来处理.
若假象在无限制硬件条件下,哈希的方式应该是最快吧.