【问题标题】:Given a file, find the ten most frequently occurring words as efficiently as possible给定一个文件,尽可能高效地找到出现频率最高的十个单词
【发布时间】:2011-05-28 13:42:17
【问题描述】:

这显然是一个面试问题(在一组面试问题中找到),但即使不是,它也很酷。

我们被告知要在所有复杂性度量上有效地做到这一点。我想创建一个 HashMap 将单词映射到它们的频率。这在时间和空间复杂度上是 O(n),但是由于可能有很多单词,我们不能假设我们可以将所有内容都存储在内存中。

我必须补充一点,问题中没有任何内容表明单词不能存储在内存中,但如果是这样的话怎么办?如果不是这样,那么这个问题似乎就没有那么具有挑战性了。

【问题讨论】:

  • 无关紧要的事情:你在哪里找到面试问题的集合?
  • 这是我朋友借给我的一本书。以下网站声称收集了谷歌面试问题。 blog.seattleinterviewcoach.com/2009/02/…
  • 将它们存储在哈希映射中是 O(n),但是如果没有后续的 O(n log n) 排序,这不会给你前 10 名。
  • @Victor 我们可以只跟踪前十个频率,只做一次线性传递。
  • “关于所有复杂性度量”?有一个 O(1) 空间(如果“10”是一个常数),O(n^2) 时间算法,以及一个 O(n) 空间和 O(n) 时间算法,那么在所有复杂性度量上哪个更好?

标签: algorithm string


【解决方案1】:

优化我自己的时间:

sort file | uniq -c | sort -nr | head -10

可能后面跟awk '{print $2}' 以消除计数。

【讨论】:

  • 这是什么意思?谁能解释一下?
  • sort 将喜欢的单词组合在一起。 uniq -c 对每个组进行计数并打印一行,其中包含计数和组。 sort -nr 排序 numerically(现在按计数)reversed 所以最高计数排在第一位。 head -10 打印前 10 名。可选的 awk 取消计数。
  • 如果一行可能包含多个单词,使用tr分割行:tr ' ' '\n' < text | sort | uniq -c | sort -nr | head -10
【解决方案2】:

我认为trie data structure 是一个选择。

在trie中,您可以记录每个节点中的字数,表示从根到当前节点的路径上由字符组成的单词的频率。

设置 trie 的时间复杂度为 O(Ln) ~ O(n)(其中 L 是最长单词中的字符数,我们可以将其视为常数)。要找到前 10 个单词,我们可以遍历 trie,这也需要 O(n)。所以解决这个问题需要O(n)。

【讨论】:

  • 其实遍历trie最坏的代价是~O(Lt),其中t是token count,不能大于N,L如上。在我看来,应该有一种从根本上更快的方法来解决这个问题。
  • @JakeKurzer 这并不是要在这些面试中成为最快的,而是要向面试官展示你能够讨论替代方案和权衡取舍。
  • “经济法则:程序员的时间很昂贵;比起机器时间更要节约。” - 罗布派克
【解决方案3】:

一个完整的解决方案是这样的:

  1. 进行外部排序 O(N log N)
  2. 统计文件中的单词freq O(N)
  3. (替代方法是使用 Trie 作为@Summer_More_More_Tea 来计算频率,如果你能负担得起那么大的内存量)O(k*N) //对于前两个步骤
  4. 使用最小堆:
    • 将前 n 个元素放入堆中
    • 对于剩下的每个单词,将其添加到堆中并删除堆中的新最小值
    • 最后堆将包含第n个最常用的单词O(|words|*log(n))

使用 Trie 的成本将是 O(k*N),因为总单词的数量通常大于词汇表的大小。最后,由于大多数西方语言的 k 较小,您可以假设线性复杂度。

【讨论】:

    【解决方案4】:

    我在 C# 中做过这样的(示例)

    int wordFrequency = 10;
    string words = "hello how r u u u u  u  u u  u  u u u  u u u u  u u u ? hello there u u u u ! great to c u there. hello .hello hello hello hello hello .hello hello hello hello hello hello ";            
    
    var result = (from word in words.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries)
                              group word by word into g
                              select new { Word = g.Key, Occurance = g.Count() }).ToList().FindAll(i => i.Occurance >= wordFrequency);
    

    【讨论】:

      【解决方案5】:

      假设我们为 26 个字母中的每一个分配一个随机素数。然后我们扫描文件。每当我们找到一个单词时,我们都会计算它的哈希值(基于位置和组成单词的字母值的公式)。如果我们在哈希表中找到这个值,那么我们肯定知道我们不是第一次遇到它,我们会增加它的键值。并维护一个最大为 10 的数组。但是如果我们遇到一个新的 hash ,那么我们存储该 hash 值的文件指针,并将 key 初始化为 0。

      【讨论】:

        【解决方案6】:

        您可以在时间/空间上进行权衡,通过计算在数据的线性传递中每次遇到某个单词时出现的次数,使用O(n^2) 获取时间,使用O(1) 获取(内存)空间。如果计数在目前找到的前 10 个以上,则保留单词和计数,否则忽略它。

        【讨论】:

        • 你必须保留每一个单词,否则每次你看到一个不在前 10 名的单词时,你会认为这是你第一次看到它。
        • 不过这样想也没关系,因为它仍然是O(n^2)。唯一的问题是它是前 10 名中的一个词(因为您可能会保留同一个词 10 次),但我们确实保留了这些词,所以这不是一个大问题。每次都认为这是第一次并不意味着我们“必须保留每一个字”。正如我上面所说,你计算“每次遇到它”。当然,时间效率不高,但 OP 想要空间效率。
        • 考虑一个文件,其中文件的后半部分在同一个词中,重复出现。如果您以线性方式处理文件,一旦您进入下半部分,您将已经拥有您保留的“前 10”字。你会看到这个词,然后数“1”。不在前10,不要留着;移动到下一个单词。再看一遍 - 我没有“保留”它,所以我没有数过它,所以数“1”。不在前10,不要留着;移动到下一个单词。重复。你能解释一下我错过了什么吗?
        • @Kirk 你描述的算法只是时间上的O(n)(或者可能是O(n log n)?)。我建议使用O(n^2) 算法,在该算法中“计算一个单词每次遇到它出现的次数”——换句话说,对于文本中的每个单词,比较文本中的每个单词并计算两者的次数是相同的。就代码而言,它看起来像for(i=0; i < num_words; ++i) { for (j=0; j < num_words; ++j) { if (words[i] == words[j]) ++count; },最后加上一些代码来处理计数是否在前 10 名中,以及该词是否已被列入前 10 名。
        【解决方案7】:

        说构建哈希并对值进行排序是最好的。我倾向于同意。 http://www.allinterview.com/showanswers/56657.html

        这是一个 Bash 实现,它做类似的事情......我认为 http://www.commandlinefu.com/commands/view/5994/computes-the-most-frequent-used-words-of-a-text-file

        【讨论】:

          【解决方案8】:

          根据输入数据的大小,保留 HashMap 可能是一个好主意,也可能不是一个好主意。例如,我们的哈希映射太大而无法放入主内存。这可能会导致大量的内存传输,因为大多数哈希映射实现需要随机访问并且在缓存上不是很好。

          在这种情况下,对输入数据进行排序会是更好的解决方案。

          【讨论】:

            【解决方案9】:

            我认为这是计数排序的典型应用,因为每个单词出现的总和等于单词的总数。带有计数排序的哈希表应该在与字数成正比的时间内完成这项工作。

            【讨论】:

              【解决方案10】:

              遍历单词字符串并将每个单词存储在字典中(使用python)并将它们作为值出现的次数。

              【讨论】:

              • Python 字典是一个 HashMap,所以这就是我建议的。
              【解决方案11】:

              如果单词列表不适合内存,您可以拆分文件直到它可以。生成每个部分的直方图(顺序或并行),并合并结果(如果您希望保证所有输入的正确性,其细节可能有点繁琐,但不应损害 O(n) 工作量,或者k 个任务的 O(n/k) 时间)。

              【讨论】:

                【解决方案12】:

                Radix tree 或其变体之一通常可让您通过折叠常用序列来节省存储空间。
                构建它需要 O(nk) - 其中 k 是“集合中所有字符串的最大长度”。

                【讨论】:

                  【解决方案13】:

                  步骤 1 :如果文件非常大并且无法在内存中排序,您可以将其拆分为可在内存中排序的块。

                  第 2 步:对于每个已排序的块计算已排序的(单词,nr_occurrence)对,此时您可以放弃这些块,因为您只需要已排序的对。

                  第 3 步:遍历块并对块进行排序,并始终保持前十位出现。

                  例子:

                  第 1 步

                  a b ab ab a b b c c ab ab

                  分成:

                  块 1:a b a ab
                  块 2: abb a a b b
                  块 3:c c ab

                  第 2 步

                  块 1:a2、b1、ab1 块 2:a2、b2、abb1
                  块 3:c2,ab2

                  第 3 步(合并块并保留前十名):

                  a4 b3 ab3 c2 abb1

                  【讨论】:

                    【解决方案14】:
                        int k = 0;
                        int n = i;
                        int j;
                        string[] stringList = h.Split(" ".ToCharArray(),
                                                      StringSplitOptions.RemoveEmptyEntries);
                        int m = stringList.Count();
                        for (j = 0; j < m; j++)
                        {
                            int c = 0;
                            for (k = 0; k < m; k++)
                            {
                                if (string.Compare(stringList[j], stringList[k]) == 0)
                                {
                                    c = c + 1;
                                }
                            }
                        }
                    

                    【讨论】:

                    • 不确定您的代码是否解决了所需的问题 - 单词未存储在内存中的情况
                    【解决方案15】:

                    在 CPU 方面不是最高效的,而且丑陋,但只用了 2 分钟就搞定了:

                    perl -lane '$h{$_}++ for @F; END{for $w (sort {$h{$b}&lt;=&gt;$h{$a}} keys %h) {print "$h{$w}\t$w"}}' file | head

                    -n循环遍历每一行
                    -a将每一行分成@F
                    每个$_字递增哈希%h
                    一旦达到fileEND
                    sort 按频率散列
                    打印频率$h{$w}和单词$w
                    使用 bash head 在 10 行处停止

                    使用此网页的文本作为输入:

                    121     the
                    77      a
                    48      in
                    46      to
                    44      of
                    39      at
                    33      is
                    30      vote
                    29      and
                    25      you
                    

                    我在一个 3.3GB 的文本文件(包含 580,000,000 个单词)上将此解决方案与顶级 shell 解决方案 (ben jackson) 进行了基准测试。
                    Perl 5.22 在 171 秒内完成,而 shell 解决方案在 474 秒内完成。

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2017-07-01
                      • 1970-01-01
                      • 2023-03-14
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2012-07-17
                      相关资源
                      最近更新 更多