【问题标题】:Algorithm to find top 10 search terms查找前 10 个搜索词的算法
【发布时间】:2011-03-16 16:42:18
【问题描述】:

我目前正在准备面试,这让我想起了我在之前的一次面试中曾经被问到的一个问题:

“您被要求设计一些软件来连续显示 Google 上的前 10 个搜索词。您可以访问一个提要,该提要提供当前在 Google 上搜索的无限实时搜索词流。描述什么算法和数据结构,你将使用它来实现这一点。你要设计两种变体:

(i) 显示有史以来排名前 10 的搜索词(即自您开始阅读提要以来)。

(ii) 仅显示过去一个月的前 10 个搜索词,每小时更新一次。

您可以使用近似值来获得前 10 名列表,但您必须证明您的选择是正确的。”
我在这次采访中被轰炸了,但仍然不知道如何实施。

第一部分要求在无限列表的不断增长的子序列中最频繁出现的 10 个项目。我研究了选择算法,但找不到任何在线版本来解决这个问题。

第二部分使用了一个有限列表,但是由于要处理的数据量很大,你不能真正将整个月的搜索词存储在内存中并每小时计算一个直方图。

由于前 10 名列表不断更新,因此问题变得更加困难,因此您需要通过滑动窗口计算前 10 名。

有什么想法吗?

【问题讨论】:

  • @BlueRaja - 这不是一个愚蠢的面试问题,这对 OP 来说是一个糟糕的解释。它不是要求无限列表中最频繁的项目,而是要求无限列表的有限子序列中最频繁的项目。继续你的类比,what is the most frequent item in the subsequence [2; 2; 3; 3; 3; 4; 4; 4; 4; 5; 5] of your sequence?
  • @BlueRaja - 这当然是一个难题,但我不明白为什么它很愚蠢 - 它似乎代表了拥有大量数据集的公司所面临的一个相当典型的问题。 @IVlad - 根据你的建议修复它,我的措辞不好!

标签: algorithm data-structures


【解决方案1】:

频率估计概述

有一些众所周知的算法可以使用固定存储量为此类流提供频率估计。一种是Frequent, Misra 和 Gries (1982)。从 n 个项目的列表中,它使用 k - 1 个计数器查找所有出现次数超过 n / k 次的项目。这是 Boyer 和 Moore 的 Majority 算法(Fischer-Salzberg,1982 年)的概括,其中 k 为 2。Manku 和 Motwani 的 LossyCounting(2002 年) ) 和 Metwally 的 SpaceSaving (2005) 算法具有相似的空间要求,但在某些条件下可以提供更准确的估计。

要记住的重要一点是,这些算法只能提供频率估计。具体来说,Misra-Gries 估计可能会低估实际频率 (n / k) 个项目。

假设您有一个算法,该算法在超过 50% 的时间出现时才能正确识别该项目。向该算法提供 N 个不同项目的流,然后添加一个项目的另一个 N - 1 个副本,x,总共 2N - 1 个项目。如果算法告诉您 x 超过总数的 50%,则它一定在第一个流中;如果不是,则 x 不在初始流中。为了让算法做出这个决定,它必须存储初始流(或一些与其长度成比例的摘要)!因此,我们可以向自己证明,这种“精确”算法所需的空间是 Ω(N)。

相反,此处描述的这些频率算法提供了一个估计值,可以识别任何超过阈值的项目,以及一些低于阈值一定幅度的项目。例如 Majority 算法,使用单个计数器,总是会给出一个结果;如果任何项目超过流的 50%,它将被找到。但它也可能给你一个只出现一次的项目。如果不对数据进行第二次传递,您将不会知道(再次使用单个计数器,但只查找该项目)。

频繁算法

这里是 Misra-Gries 的 Frequent 算法的简单描述。 Demaine (2002) 和其他人已经优化了算法,但这为您提供了要点。

指定阈值分数,1 / k;将找到出现超过 n / k 次的任何项目。创建一个空地图(如红黑树);键将是搜索词,值将是该词的计数器。

  1. 查看流中的每个项目。
  2. 如果地图中存在该术语,则增加关联的计数器。
  3. 否则,如果地图少于 k - 1 个条目,则将术语添加到地图中,计数器为 1。
  4. 但是,如果映射已经有 k - 1 个条目,则减少每个条目中的计数器。如果在此过程中任何计数器达到零,请将其从地图中移除。

请注意,您可以使用固定存储量(仅是固定大小的地图)处理无限量的数据。所需的存储量仅取决于感兴趣的阈值,流的大小无关紧要。

计算搜索次数

在这种情况下,您可能会缓冲一小时的搜索,并对该小时的数据执行此过程。如果您可以对这一小时的搜索日志进行第二次遍历,您可以获得在第一次遍历中确定的顶级“候选人”出现的准确计数。或者,也许可以通过一次并报告所有候选人,知道任何应该存在的项目都包括在内,任何额外的只是噪音,会在接下来的一个小时内消失。

任何确实超过兴趣阈值的候选人都会被存储为摘要。保留一个月的这些摘要,每小时丢弃最旧的,这样您就可以很好地近似最常见的搜索词。

【讨论】:

  • 我相信这个解决方案可以起到过滤器的作用,减少您感兴趣的搜索词的数量。如果某个术语出现在地图中,请开始跟踪它的实际统计信息,即使它掉出地图。然后,您可以跳过对数据的第二次遍历,并从您收集的有限统计数据中生成排序前 10 名。
  • 我喜欢通过递减计数器从树中修剪搜索较少的术语的优雅方式。但是,一旦地图“满了”,是否需要对每个到达的新搜索词都进行递减步骤?一旦这种情况开始发生,这会不会导致新的搜索词在他们的计数器有机会充分增加之前迅速从地图中删除?
  • @del - 请记住,此算法用于定位超过指定阈值频率的术语,不一定用于查找最常见的术语。如果最常见的术语低于指定的阈值,则通常不会找到它们。您对“过快”删除较新术语的担忧可能与此案例有关。看待这一点的一种方法是,流行中有真正的“信号”,它们会明显地从“噪音”中脱颖而出。但有时,找不到信号,只是随机搜索静态。
  • @erickson - 是的 - 我得到的是这个算法的假设是前 10 个单词均匀分布在测量窗口中。但只要您保持测量窗口足够小(例如 1 小时),这可能是一个有效的假设。
  • @erickson,虽然 uniform 分布不是必需的,但我想知道这在更现实的分布(幂律,Zipf)中如何工作。假设我们有 N 个不同的词,并保留容量为 K 的红黑树,希望它以 K 个最频繁的词结束。如果 (N - K) 个词的词项的累积频率大于 K 个最频繁词的累积频率,则最终的树保证包含垃圾。你同意吗?
【解决方案2】:

好吧,看起来数据量非常大,存储所有频率的成本可能很高。 当数据量太大无法全部存储时,我们进入数据流算法领域

这方面有用的书: Muthukrishnan - "Data Streams: Algorithms and Applications"

与我从上面挑选的手头问题密切相关的参考: Manku, Motwani - "Approximate Frequency Counts over Data Streams" [pdf]

顺便说一句,斯坦福大学的 Motwani(编辑)是非常重要的 "Randomized Algorithms" 书的作者。 本书的第 11 章处理了这个问题编辑:对不起,不好的参考,那个特定的章节是关于不同的问题。查过之后,我转而推荐section 5.1.2 of Muthukrishnan's book,可以在线获取。

嘿,很好的面试问题。

【讨论】:

  • +1 非常有趣的东西,网站上应该有一种方法来标记“阅读”的东西。感谢分享。
  • @Gollum:我的书签中有一个待读文件夹;你可以这样做。我知道这些链接正在添加到我的 :)
  • +1。流式算法正是这里的主题,Muthu 的书(迄今为止唯一写的书,AFAIK)很棒。
  • +1。相关:en.wikipedia.org/wiki/Online_algorithm。顺便说一句,Motwani 最近去世了,所以也许作者更准确。
  • 很奇怪。我是从书中认识他的,但他肯定因为这个而出名:“Motwani 是有关 PageRank 算法的一篇有影响力的早期论文的合著者之一(与 Larry Page 和 Sergey Brin 以及 Terry Winograd), Google 搜索技术的基础。" (en.wikipedia.org/wiki/Rajeev_Motwani)
【解决方案3】:

这是我目前正在进行的研究项目之一。这个要求几乎和你的一样,我们已经开发了很好的算法来解决这个问题。

输入

输入是无穷无尽的英语单词或短语(我们将它们称为tokens)。

输出

  1. 输出我们见过的前 N ​​个令牌 远(从我们拥有的所有代币 见过!)
  2. 在 a 中输出前 N 个标记 历史窗口,例如,最后一天或 上周。

这项研究的一个应用是在 Twitter 或 Facebook 中查找热门话题或话题趋势。我们有一个在网站上爬行的爬虫,它会生成一个词流,然后输入到系统中。然后系统将输出总体上或历史上频率最高的单词或短语。想象一下,在过去的几周里,“世界杯”这个词会在 Twitter 上出现很多次。 “章鱼保罗”也是如此。 :)

字符串转换成整数

系统对每个单词都有一个整数 ID。虽然网上有几乎无限可能的词,但是在积累了一大堆词之后,找到新词的可能性越来越低。我们已经找到了 400 万个不同的单词,并为每个单词分配了一个唯一的 ID。这整组数据可以作为哈希表加载到内存中,大约消耗 300MB 内存。 (我们已经实现了自己的哈希表,Java的实现占用了巨大的内存开销)

然后可以将每个短语识别为整数数组。

这很重要,因为整数的排序和比较比字符串快得多

存档数据

系统为每个令牌保留存档数据。基本上是成对的(Token, Frequency)。但是,存储数据的表会非常大,以至于我们必须对表进行物理分区。一旦分区方案基于令牌的 ngram。如果token是一个单词,就是1gram。如果token是两词短语,则为2gram。这种情况还在继续。大约 4 克,我们有 10 亿条记录,表大小约为 60GB。

处理传入流

系统会吸收输入的句子,直到内存被充分利用(是的,我们需要一个 MemoryManager)。在取出 N 个句子并存储在内存中后,系统会暂停,并开始将每个句子标记为单词和短语。每个标记(单词或短语)都被计算在内。

对于频繁使用的令牌,它们始终保存在内存中。对于不太频繁的标记,它们根据 ID 进行排序(记住我们将字符串转换为整数数组),并序列化到磁盘文件中。

(但是,对于您的问题,由于您只计算单词,因此您可以仅将所有单词频率图放在内存中。精心设计的数据结构对于 400 万个不同的单词仅消耗 300MB 内存。一些提示:使用ASCII char 来表示字符串),这是可以接受的。

同时,一旦找到系统生成的任何磁盘文件,就会激活另一个进程,然后开始合并它。由于磁盘文件已排序,因此合并将采用类似于合并排序的过程。这里也需要注意一些设计,因为我们想避免过多的随机磁盘寻道。这个想法是避免同时读取(合并进程)/写入(系统输出),让合并进程从一个磁盘读取,同时写入另一个磁盘。这类似于实现锁定。

一天结束

在一天结束时,系统将在内存中存储许多具有频率的频繁令牌,并将许多其他不太频繁的令牌存储在多个磁盘文件中(并且每个文件都被排序)。

系统将内存中的映射刷新到磁盘文件中(对其进行排序)。现在,问题变成了合并一组排序的磁盘文件。使用类似的过程,我们会在最后得到一个排序的磁盘文件。

然后,最后的任务是将排序后的磁盘文件合并到存档数据库中。 取决于存档数据库的大小,如果足够大,算法的工作原理如下:

   for each record in sorted disk file
        update archive database by increasing frequency
        if rowcount == 0 then put the record into a list
   end for

   for each record in the list of having rowcount == 0
        insert into archive database
   end for

直觉是,一段时间后,插入的次数会越来越少。越来越多的操作将只更新。并且这个更新不会受到索引的惩罚。

希望整个解释会有所帮助。 :)

【讨论】:

  • 我不明白。在单词的整数 ID 中可以做什么样的有意义的排序或比较?数字不是随意的吗?
  • 此外,计算单词的频率是 Google MapReduce 论文 (labs.google.com/papers/mapreduce.html) 中的第一个示例,只需几行代码即可解决。您甚至可以将您的数据移动到 google app angine 并执行这样的 MapReduce (code.google.com/p/appengine-mapreduce)
  • @Dimitris Andreou:对整数进行排序在字符串上会更快。这是因为比较两个整数比比较两个字符串要快。
  • @Dimitris Andreou:Google 的 mapreduce 是解决这个问题的一个很好的分布式方法。啊!感谢您提供链接。是的,我们最好使用多台机器进行排序。不错的方法。
  • @Dimitris Andreou:到目前为止,我只考虑单机排序方法。在分布中排序真是个好主意。
【解决方案4】:

您可以将hash tablebinary search tree 结合使用。实现一个<search term, count> 字典,它告诉你每个搜索词被搜索了多少次。

显然,每小时迭代整个哈希表以获得前 10 名是非常糟糕的。但这是我们正在谈论的谷歌,所以你可以假设前十名都会获得,比如超过 10 000 次点击(虽然这可能是一个更大的数字)。因此,每当搜索词的计数超过 10 000 时,将其插入 BST。然后每个小时,您只需从 BST 获取前 10 个,其中应该包含相对较少的条目。

这解决了前 10 名的问题。


真正棘手的部分是处理一个术语在月度报告中的位置(例如,“堆栈溢出”在过去两个月可能有 50 000 次点击,但过去一个月只有 10 000 次,而“amazon”过去两个月可能有 40 000,但过去一个月有 30 000。您希望“亚马逊”出现在您的月度报告中的“堆栈溢出”之前)。为此,我将为所有主要(超过 10 000 个历史搜索)搜索词存储一个 30 天的列表,该列表告诉您该词每天被搜索了多少次。该列表将像 FIFO 队列一样工作:您删除第一天并每天(或每小时)插入一个新的,但是您可能需要存储更多信息,这意味着更多的内存/空间。如果内存不是问题,请执行它,否则就使用他们正在谈论的那个“近似值”)。

这看起来是一个好的开始。然后,您可以担心修剪具有> 10 000次点击但很长时间没有很多点击的术语以及类似的东西。

【讨论】:

    【解决方案5】:

    案例 i)

    为所有搜索词维护一个哈希表,以及一个与哈希表分开的前十名列表。每当发生搜索时,递增哈希表中的相应项目并检查该项目现在是否应该与前十名列表中的第 10 项进行切换。

    O(1) 查找前十名列表,最大 O(log(n)) 插入哈希表(假设冲突由自平衡二叉树管理)。

    案例二) 我们不是维护一个巨大的哈希表和一个小列表,而是维护一个哈希表和所有项目的排序列表。每当进行搜索时,该术语在散列表中递增,并且在排序列表中可以检查该术语以查看它是否应该与它之后的术语切换。自平衡二叉树可以很好地解决这个问题,因为我们还需要能够快速查询它(稍后会详细介绍)。

    此外,我们还以 FIFO 列表(队列)的形式维护“小时”列表。每个“小时”元素将包含在该特定小时内完成的所有搜索的列表。例如,我们的小时列表可能如下所示:

    Time: 0 hours
          -Search Terms:
              -free stuff: 56
              -funny pics: 321
              -stackoverflow: 1234
    Time: 1 hour
          -Search Terms:
              -ebay: 12
              -funny pics: 1
              -stackoverflow: 522
              -BP sucks: 92
    

    然后,每小时:如果列表至少有 720 小时(即 30 天的小时数),查看列表中的第一个元素,对于每个搜索词,将哈希表中的该元素递减适量。然后,从列表中删除第一个小时元素。

    假设我们现在是第 721 小时,我们已经准备好查看列表中的第一个小时(上图)。我们将哈希表中的免费内容减少 56,有趣的图片减少 321,等等,然后从列表中完全删除第 0 小时,因为我们再也不需要查看它了。

    我们维护允许快速查询的所有术语的排序列表的原因是,在我们检查 720 小时前的搜索术语之后的每一小时,我们需要确保前十名列表保持排序。例如,当我们在哈希表中将“免费的东西”减 56 时,我们会检查它现在在列表中的位置。因为它是一棵自平衡二叉树,所有这些都可以在 O(log(n)) 时间内很好地完成。


    编辑:牺牲精度换取空间...

    在第一个中也实现一个大列表可能很有用,就像在第二个中一样。然后我们可以在这两种情况下应用以下空间优化:运行 cron 作业以删除列表中除顶部 x 项之外的所有项目。这将降低空间需求(从而使列表上的查询更快)。当然,它会产生一个近似的结果,但这是允许的。 x 可以在部署应用程序之前根据可用内存进行计算,并在有更多可用内存时动态调整。

    【讨论】:

      【解决方案6】:

      粗略的思考...

      前十名

      • 使用存储每个术语的计数的哈希集合(清理术语等)
      • 一个排序后的数组,其中包含正在进行的前 10 个,每当术语的计数等于或大于数组中的最小计数时,就会将术语/计数添加到此数组中

      每月前 10 名每小时更新:

      • 使用一个数组索引自开始后经过的小时数模 744(一个月中的小时数),该数组条目由哈希集合组成,其中存储了在此小时槽期间遇到的每个术语的计数。每当时隙计数器发生变化时,都会重置条目

      Err... 有道理吗?我没有像在现实生活中那样考虑过这一点

      啊,是的,忘了提一下,每月统计数据所需的每小时“复制/展平”实际上可以重用用于历史前 10 名的相同代码,这是一个很好的副作用。

      【讨论】:

        【解决方案7】:

        精确解

        首先,一种保证正确结果的解决方案,但需要大量内存(大地图)。

        “所有时间”变体

        维护一个哈希映射,查询作为键,计数作为值。此外,保留迄今为止最频繁的 10 个查询的列表以及第 10 个最频繁的计数的计数(阈值)。

        在读取查询流时不断更新地图。每次计数超过当前阈值时,请执行以下操作:从“前 10”列表中删除第 10 个查询,将其替换为您刚刚更新的查询,并更新阈值。

        “上个月”变体

        保持相同的“前 10 名”列表并以与上述相同的方式对其进行更新。另外,保留一张类似的地图,但这次将 30*24 = 720 个计数(每小时一个)的向量存储为值。每小时对每个键执行以下操作:从向量中删除最旧的计数器,在末尾添加一个新计数器(初始化为 0)。如果向量全为零,则从映射中删除键。此外,您必须每小时从头计算“Top 10”列表。

        注意:是的,这次我们存储 720 个整数而不是 1 个,但是键少得多(历史变体有一个真的长尾)。

        近似值

        这些近似值并不能保证正确的解决方案,但会减少内存消耗。

        1. 处理每第 N 个查询,跳过其余查询。
        2. (仅适用于所有时间变体)在映射中最多保留 M 个键值对(M 应该尽可能大)。这是一种 LRU 缓存:每次读取不在地图中的查询时,删除最近最少使用的查询,计数为 1,并将其替换为当前处理的查询。

        【讨论】:

        • 我喜欢近似 1 中的概率方法。但是使用近似 2(LRU 缓存),如果最初不太流行的术语后来变得流行怎么办?每次添加时它们不会被丢弃,因为它们的数量会非常少吗?
        • @del 没错,第二个近似值仅适用于某些查询流。它不太可靠,但同时需要更少的资源。注意:您也可以结合这两种近似值。
        【解决方案8】:

        过去一个月的前 10 个搜索词

        使用内存高效的索引/数据结构,例如tightly packed tries(来自tries 上的维基百科条目)大致定义了内存需求和n - 项数之间的某种关系。

        如果所需内存可用(假设 1),您可以保留准确的每月统计数据,并将每个月汇总到所有时间统计数据中。

        这里还有一个假设,将“上个月”解释为固定窗口。 但是即使每月窗口是滑动的,上面的过程也显示了原理(滑动可以用给定大小的固定窗口来近似)。

        这让我想起了round-robin database,除了一些统计数据是在“所有时间”上计算的(从某种意义上说,并非所有数据都被保留;rrd 通过平均、总结或选择最大/最小来合并时间段而不考虑细节值,在给定的任务中丢失的细节是关于低频项目的信息,这可能会引入错误)。

        假设 1

        如果我们不能保持整个月的完美统计,那么我们应该能够找到我们应该能够保持完美统计的某个时期P。 例如,假设我们对某个时间段 P 有完美的统计数据,该时间段进入 n 个月。
        完美的统计定义函数f(search_term) -> search_term_occurance.

        如果我们可以将所有n 完美的统计表保存在内存中,那么可以这样计算滑动月度统计:

        • 添加最新时期的统计数据
        • 删除最旧时期的统计数据(因此我们必须保留n 完美的统计数据表)

        但是,如果我们只保留汇总级别(每月)的前 10 名,那么我们将能够从固定时期的完整统计数据中丢弃大量数据。这已经给出了一个工作过程,该过程具有固定的内存需求(假设周期 P 的完美统计表的上限)。

        上述过程的问题在于,如果我们只保留滑动窗口的前 10 个词的信息(一直以来都是如此),那么对于在一段时间内达到峰值的搜索词,统计数据将是正确的,但可能看不到随着时间不断流入的搜索词的统计数据。

        这可以通过保留超过前 10 个术语的信息来抵消,例如前 100 个术语,希望前 10 个是正确的。

        我认为进一步的分析可以将条目成为统计数据的一部分所需的最小出现次数(这与最大错误有关)。

        (在决定哪些条目应成为统计数据的一部分时,人们还可以监控和跟踪趋势;例如,如果对每个时期 P 中每个术语的出现线性推断告诉您该术语将在一个月内变得重要一两个您可能已经开始跟踪它。类似的原则适用于从跟踪池中删除搜索词。)

        上述情况的最坏情况是当您有很多几乎相同频率的术语并且它们一直在变化时(例如,如果只跟踪 100 个术语,那么如果前 150 个术语出现的频率相同,但前 50 个术语在第一个月,而且通常会晚一些时间,然后统计数据将无法正确保存)。

        还有另一种方法,它的内存大小不固定(严格来说,上面也不是),它会根据事件/时间段(日、月、年、所有时间)定义最小重要性保持统计数据。这可以保证聚合期间每个统计数据的最大误差(再次参见循环)。

        【讨论】:

          【解决方案9】:

          "clock page replacement algorithm" 的改编版(也称为“第二次机会”)怎么样?如果搜索请求均匀分布,我可以想象它会很好地工作(这意味着大多数搜索词会定期出现,而不是连续出现 5mio 次,然后再也不会出现)。

          这是算法的可视化表示:

          【讨论】:

            【解决方案10】:

            当您拥有固定数量的内存和“无限”(认为非常大)令牌流时,这个问题并不是普遍可解决的。

            粗略的解释...

            要了解原因,请考虑一个令牌流,它在输入流中每 N 个令牌都有一个特定令牌(即单词)T。

            另外,假设内存最多可以保存对 M 个标记的引用(单词 id 和计数)。

            在这些条件下,可以构造一个输入流,其中如果 N 足够大以至于流在 T 之间包含不同的 M 个令牌,则永远不会检测到令牌 T。

            这与 top-N 算法细节无关。它只取决于极限 M。

            要了解为什么会这样,请考虑由两个相同令牌组组成的传入流:

            T a1 a2 a3 ... a-M T b1 b2 b3 ... b-M ...
            

            其中 a 和 b 都是不等于 T 的有效标记。

            请注意,在此流中,对于 a-i 和 b-i,T 出现了两次。然而,它似乎很少从系统中清除。

            从一个空的内存开始,第一个令牌(T)将占用内存中的一个槽(以 M 为界)。然后a1会消耗一个slot,当M用完时一直到a-(M-1)。

            当 a-M 到达时,算法必须删除一个符号,所以让它成为 T。 下一个符号将是 b-1,这将导致 a-1 被刷新,等等。

            因此,T 不会在内存中停留足够长的时间来建立真正的计数。简而言之,任何算法都会错过一个局部频率足够低但全局频率高(在流的长度上)的令牌。

            【讨论】:

              【解决方案11】:

              将搜索项的计数存储在一个巨大的哈希表中,其中每次新搜索都会导致特定元素加一。跟踪前 20 个左右的搜索词;当第 11 位的元素递增时,检查它是否需要与 #10* 交换位置(不必保持前 10 位的排序;您只关心区分第 10 位和第 11 位)。

              *需要进行类似的检查以查看新的搜索词是否在第 11 位,所以这个算法也会冒泡到其他搜索词——所以我稍微简化了一点。

              【讨论】:

              • 您需要限制哈希表的大小。如果你得到一个独特的搜索流怎么办?您需要确保不会阻止自己注意到经常但不经常搜索的术语。随着时间的推移,这可能是最热门的搜索词,特别是如果所有其他搜索词都是“时事”,即现在搜索了很多,但下周搜索量不大。实际上,像这样的考虑可能是您想要做出的近似值。为他们辩解说,我们不会捕捉到这类事情,因为这样做会使算法的时间/空间成本更高。
              • 我很确定 Google 对 everything 都有计数 - 但有些计数不是静态维护的,而是根据需要计算的。
              【解决方案12】:

              有时最好的答案是“我不知道”。

              我会采取更深的刺。我的第一直觉是将结果输入 Q。一个过程将不断处理进入 Q 的项目。该过程将维护一个

              的映射

              术语 -> 计数

              每次处理 Q 项时,您只需查找搜索词并增加计数。

              同时,我会维护一个对地图中前 10 个条目的引用列表。

              对于当前实现的条目,查看其计数是否大于前 10 名中最小条目的计数。(如果不在列表中)。如果是,请将最小的替换为条目。

              我认为这会奏效。没有任何操作是时间密集型的。您必须找到一种方法来管理计数图的大小。但这对于面试答案来说应该足够了。

              他们并不期待一个解决方案,而是想看看你是否能思考。您不必当时就在那里编写解决方案....

              【讨论】:

              • 这个数据结构叫做queueQ是一个字母:)。
              • 如果我进行采访,“我不知道”肯定不是最好的答案。想想你的脚。如果您不知道,请弄清楚 - 或者至少尝试一下。
              • 在面试中,当我看到某人在他们的 7 页简历中使用了 hibernate 5 次,并且他们无法告诉我 ORM 是什么,我立即结束面试。我宁愿他们不把它写在简历上,只是说:“我不知道”。没有人知道一切。 @IVIAd,我假装自己是一名 C 开发人员并试图节省一些东西……;)
              【解决方案13】:

              一种方法是,对于每次搜索,您都存储该搜索词及其时间戳。这样一来,只要比较给定时间段内的所有搜索词,就可以找到任何时间段内的前十名。

              算法简单,但缺点是内存和时间消耗较大。

              【讨论】:

                【解决方案14】:

                使用具有 10 个节点的 Splay Tree 怎么样?每次您尝试访问树中未包含的值(搜索词)时,丢弃任何叶子,插入该值并访问它。

                这背后的想法与我的另一个answer 相同。在平均/定期访问搜索词的假设下,此解决方案应该表现得非常好。

                编辑

                人们还可以在树中存储更多搜索词(我在其他答案中建议的解决方案也是如此),以便不删除可能很快再次访问的节点。存储的值越多,结果就越好。

                【讨论】:

                  【解决方案15】:

                  不知道我是否理解正确。 我的解决方案是使用堆。 由于前 10 个搜索项,我构建了一个大小为 10 的堆。 然后用新的搜索更新这个堆。如果新搜索的频率大于堆(最大堆)顶部,则更新它。放弃频率最小的那个。

                  但是,如何计算特定搜索的频率将取决于其他内容。 或许正如大家所说,数据流算法....

                  【讨论】:

                    【解决方案16】:

                    使用 cm-sketch 存储自开始以来所有搜索的计数,并为前 10 保留一个大小为 10 的最小堆。 对于每月结果,保留 30 cm-sketch/hash-table 和 min-heap,每个从最后 30、29 ..、1 天开始计数和更新。作为一天过去,清除最后一天并将其用作第一天。 每小时结果相同,保留 60 个哈希表和最小堆并开始计算最后 60、59、...1 分钟。作为一分钟通过,清除最后一个并将其用作第 1 分钟。

                    月结果精确到 1 天,每小时结果精确到 1 分钟

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2020-11-20
                      • 2012-07-17
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多