在非常大的文件中查找最常见的三项序列

时间:2011-12-30 19:12:00

标签: c++ c algorithm data-structures hash

我有很多网页访问日志文件,其中每次访问都与用户ID和时间戳相关联。我需要确定最受欢迎(即最常访问)的三页序列。日志文件太大,无法一次保存在主内存中。

示例日志文件:

User ID  Page ID
A          1
A          2
A          3
B          2
B          3
C          1
B          4
A          4

相应的结果:

  

A:1-2-3,2-3-4
  B:2-3-4
  2-3-4是最受欢迎的三页序列

我的想法是使用两个哈希表。用户ID的第一个哈希并存储其序列;第二个散列三页序列并存储每个序列出现的次数。这需要O(n)空间和O(n)时间。

但是,由于我必须使用两个哈希表,因此内存不能同时保存所有内容,而且我必须使用磁盘。经常访问磁盘效率不高。

我怎样才能更好地做到这一点?

5 个答案:

答案 0 :(得分:4)

如果您想快速获得近似结果,请按预期使用哈希表,但为每个哈希表添加一个有限大小的队列,以删除最近最少使用的条目。

如果您需要精确的结果,请使用外部排序程序按用户ID对日志进行排序,然后将每3个连续的条目组合并再次排序,这次是按页面ID。

更新(按时间戳排序)

可能需要进行一些预处理才能正确使用日志文件的时间戳:

  • 如果日志文件已经按时间戳排序,则不需要预处理。
  • 如果有多个日志文件(可能来自独立进程),并且每个文件已按时间戳排序,请打开所有这些文件并使用合并排序来读取它们。
  • 如果文件几乎按时间戳排序(好像几个独立进程将日志写入单个文件),请使用二进制堆以正确的顺序获取数据。
  • 如果文件未按时间戳排序(实际上不太可能),请按时间戳使用外部排序。

更新2(改进近似方法)

使用LRU队列的近似方法应该为随机分布的数据产生相当好的结果。但是,网页访问在一天中的不同时间可能会有不同的模式,或者在周末可能会有所不同。最初的方法可能会给这些数据带来不良结果。为了改善这一点,可以使用分层LRU队列。

将LRU队列分区为log(N)个较小的队列。大小为N / 2,N / 4,......最大的一个应该包含任何元素,下一个 - 仅元素,至少看到2次,下一个 - 至少4次,...如果元素从某个子元素中删除-queue,它被添加到另一个,因此它在完全删除之前存在于层次结构较低的所有子队列中。这样的优先级队列仍然具有O(1)复杂度,但允许对大多数流行页面进行更好的近似。

答案 1 :(得分:3)

这里可能存在语法错误,但这应该占用有限数量的RAM,用于几乎无限长度的日志文件。

typedef int pageid;
typedef int userid;
typedef pageid[3] sequence;
typedef int sequence_count;

const int num_pages = 1000; //where 1-1000 inclusive are valid pageids
const int num_passes = 4;
std::unordered_map<userid, sequence> userhistory;
std::unordered_map<sequence, sequence_count> visits;
sequence_count max_count=0;
sequence max_sequence={};
userid curuser;
pageid curpage;
for(int pass=0; pass<num_passes; ++pass) { //have to go in four passes
    std::ifstream logfile("log.log");
    pageid minpage = num_pages/num_passes*pass; //where first page is in a range
    pageid maxpage = num_pages/num_passes*(pass+1)+1;
    if (pass==num_passes-1) //if it's last pass, fix rounding errors
        maxpage = MAX_INT;
    while(logfile >> curuser >> curpage) { //read in line
        sequence& curhistory = userhistory[curuser]; //find that user's history
        curhistory[2] = curhistory[1];
        curhistory[1] = curhistory[0];
        curhistory[0] = curhistory[curpage]; //push back new page for that user
        //if they visited three pages in a row
        if (curhistory[2] > minpage && curhistory[2]<maxpage) { 
            sequence_count& count = visits[curhistory]; //get times sequence was hit
            ++count; //and increase it
            if (count > max_count) { //if that's new max
                max_count = count;  //update the max
                max_sequence = curhistory; //arrays, so this is memcpy or something
            }
        }
    }
}
std::cout << "The sequence visited the most is :\n";
std::cout << max_sequence[2] << '\n';
std::cout << max_sequence[1] << '\n';
std::cout << max_sequence[0] << '\n';
std::cout << "with " << max_count << " visits.\n";

请注意,如果pageiduseridstring而不是int s,则会对速度/大小/缓存造成重大影响。

[EDIT2]它现在可以在4个(可自定义的)传递中工作,这意味着它使用更少的内存,使这在RAM中实际工作。它只是比较慢。

答案 2 :(得分:2)

如果您有1000个网页,那么您有10亿个可能的3页序列。如果你有一个简单的32位计数器数组,那么你将使用4GB的内存。可能有办法通过丢弃数据来削减这一点,但如果你想保证得到正确答案,那么这总是最糟糕的情况 - 没有避免它,并且发明了节省内存的方法。平均情况会使最坏的情况更加耗费内存。

最重要的是,您必须跟踪用户。对于每个用户,您需要存储他们访问过的最后两页。假设用户在日志中按名称引用,则需要将用户名称存储在哈希表中,加上两个页码,所以假设每个用户平均24个字节(可能是保守的 - 我假设短用户名)。 1000个用户将是24KB; 1000000用户24MB。

显然,序列计数器主导了内存问题。

如果你只有1000页,那么4GB内存在现代64位机器中并不合理,特别是在磁盘支持的虚拟内存量很大的情况下。如果你没有足够的交换空间,你可以创建一个mmapped文件(在Linux上 - 我认为Windows有类似的东西),并且依赖操作系统来总是将大多数用例缓存在内存中。

所以,基本上,数学要求如果你有大量的网页需要跟踪,并且你希望能够应对最坏的情况,那么你将不得不接受你必须使用磁盘文件。

我认为有限容量的哈希表可能是正确的答案。您可以根据可用内存大小调整特定机器的优化程序。有了这个,你需要处理表达到容量的情况。如果您很少到达那里,它可能不需要非常有效。以下是一些想法:

  • 将最不常用的序列逐出文件,保留最常用的内存。我需要在桌子上两次通过以确定什么级别低于平均水平,然后进行驱逐。不管怎么说,每当你遇到哈希错过时,你都需要知道每个条目的位置,这可能会很棘手。

  • 只需将整个表转储到文件中,然后从头开始构建一个新表。重复。最后,重新组合所有表中的匹配条目。最后一部分也可能证明是棘手的。

  • 使用mmapped文件扩展表。确保该文件主要用于最常用的序列,如我的第一个建议。基本上,你只是将它用作虚拟内存 - 在忘记地址之后,文件将毫无意义,但你不需要保留那么久。我假设这里没有足够的常规虚拟内存,和/或你不想使用它。显然,这仅适用于64位系统。

答案 3 :(得分:1)

我认为您只需为每个用户ID存储最近看到的三元组吗? 所以你有两个哈希表。第一个包含userid的键,最近看到的三元组的值大小等于用户ID的数量。

编辑:假设文件已按时间戳排序。

第二个哈希表有一个userid键:page-triple,以及一个看到的次数值。

我知道你说c ++但是这里有一些awk在一次传递中做到这一点(应该很简单地转换为c ++):

#  $1 is userid, $2 is pageid

{
    old = ids[$1];          # map with id, most-recently-seen triple
    split(old,oldarr,"-"); 
    oldarr[1]=oldarr[2]; 
    oldarr[2]=oldarr[3]; 
    oldarr[3] = $2; 
    ids[$1]=oldarr[1]"-"oldarr[2]"-"oldarr[3]; # save new most-recently-seen
    tripleid = $1":"ids[$1];  # build a triple-id of userid:triple
    if (oldarr[1] != "") { # don't accumulate incomplete triples
        triples[tripleid]++; }   # count this triple-id
}
END {
    MAX = 0;
    for (tid in  triples) {
        print tid" "triples[tid];
        if (triples[tid] > MAX) MAX = tid;
    }
    print "MAX is->" MAX" seen "triples[tid]" times";
}

答案 4 :(得分:1)

如果您使用的是Unix,sort命令可以处理任意大的文件。所以你可以这样做:

  1. sort -k1,1 -s logfile > sorted(请注意,这是第一列的稳定排序(-s))
  2. 执行sorted的自定义处理,使用C ++或shell脚本将每个三元组作为新行输出到另一个文件,例如triplets。因此,在给出的示例中,您将获得包含三行的文件:1-2-3,2-3-4,2-3-4。此处理很快,因为步骤1意味着您一次只处理一个用户的访问,因此您可以一次处理sorted个文件。
  3. sort triplets | uniq -c | sort -r -n | head -1应该给出最常见的三元组及其计数(它对三元组进行排序,计算每个三元组的出现次数,按照计数的降序对它们进行排序,然后选择最高的三元组)。
  4. 这种方法可能没有最佳性能,但它不应该耗尽内存。