【问题标题】:How to find nth occurence (in reverse) of a word in a large text file?如何在大文本文件中查找单词的第 n 次出现(反向)?
【发布时间】:2012-12-30 08:55:36
【问题描述】:

这是一个面试问题,对效率的担忧。当有一个非常大的文件(以 GB 为单位)时,类似于日志文件。我们如何从文件末尾找到第 10 次出现“error”或“java”等单词。我只能想到扫描整个文件,然后以相反的顺序找出发生的情况。但我认为这不是正确的做法! (最好用 C 或 Java 编码)

我还想知道另一件事。当面试官特别提到它是一个非常大的文件时,我们在开始编写代码时应该考虑哪些因素(除了记住扫描是非常昂贵的事情)

【问题讨论】:

  • 如何查看文件 I/O 函数,看看是否有任何可以让您向后读取文件的方法,也许是先将读取位置设置在文件末尾?
  • 至于您的第二段,您可能需要读取位(10-100 mb 块?),而不是将整个文件加载到内存中。或者这可能是他的说法,“不要从头开始阅读”!
  • 看看mattfleming.com/node/11向后读取一个文本文件

标签: java c string algorithm large-files


【解决方案1】:

为了在大文本中搜索单词,Boyer Moore 算法被广泛使用。

原理(见链接以获取实时示例):在文件中的某个位置(索引)开始比较时,如果正在比较的文本的第一个字母根本不在正在搜索的单词中,则没有需要将其其他 [wordLength - 1] 个字符与文本进行比较,索引可以向前移动单词长度。如果字母在单词中,不完全是在这里,而是移动了几个字符,比较也可以移动几个字符等......

  • 根据字长和与文本的相似度,搜索可能会加快很多(最高可达 naiveSearchTime / wordLength)。

edit 由于您从文件末尾搜索,因此首先要比较单词的第一个字母(不是最后一个)。例如。在“2001 a space odyssey”中搜索“space”,单词space's'将与odyssey第一个'y'进行比较。下一个比较是相同的 's' 与文本 space 'c'。
最后,为了找到第 n 次出现,每次找到单词时都会减少一个简单的计数器(初始化为 n),当它达到 0 时,就是这样。

该算法易于理解和实现。面试的理想选择。

您可能还会问该文件是只搜索一次还是多次?如果打算多次搜索,您可以建议从文件中索引单词。 IE。在内存中创建一个结构,可以快速找到一个单词是否在其中,在哪里,多少次等等......我喜欢Trie algorithm,也很容易理解,而且非常快(也可能是非常贪婪的内存也取决于文本)。它的复杂度是O(wordLength)

--

当面试官提到“非常大的文件”时,需要考虑很多因素,比如

  • 搜索算法如上
  • 文本可以放入内存吗? (例如在处理所有文件时)我是否必须实现文件搜索算法(即一次只使用内存中的部分文件)
  • 文件在哪里?内存(快速)、硬盘(较慢但至少在本地)、远程(通常较慢、连接问题、远程访问、防火墙、网络速度等)
  • 文件被压缩了吗? (解压后会占用更多空间)
  • 文件是由一个文件还是几个块组成的?
  • 它包含文本还是二进制文件?如果是文本,其语言会指示字母出现的概率(例如,在英语中,Y 出现的频率比在法语中要高得多)。
  • 如果相关,提供索引文件单词
  • 提议从 big-one 创建一个更简单的文件(例如删除重复的单词等...),以便获得更容易处理的更小文件

...

【讨论】:

    【解决方案2】:

    这个问题的答案有两个部分。一种是使用的算法,它可以是任何好的字符串搜索算法(Boyer Moore / KMP / Trie 等)。另一部分是IO。 为了加快速度,因为您无法真正从文件中向后读取,一个好的方法是:

    1. 分配一块内存,比如 10MB
    2. for (i = 1; (filesize - 10MB * i) >= 0; i++) {
    3. 寻求(文件大小 - 10MB * i)并将 10MB 读入内存
    4. 在当前块中搜索字符串向后并增加一个计数器
    5. 当计数器达到 10 时停止

    这是一个面向 IO 的问题,您可以使用多线程系统或多台机器改进此方法,在这些机器中,您可以并行执行搜索和从文件读取到内存(即步骤 3 和 4)。

    这是 C++ 代码,但您知道如何在 Java 中完成。

    【讨论】:

    • 请告诉我为什么是-1。
    • 你应该修复几个错误:首先,你永远不会读取文件的第一个字节,除非它恰好是 10MB 的偶数倍,所以如果第 10 次出现恰好接近开始,你会想念它。其次,您没有解释如何匹配恰好跨越两个连续 10MB 块的事件;你的算法的严格实现会错过这些。 (如果您尝试并行进行搜索尤其棘手,但如果文件全部存储在单个磁盘上,并行读取不太可能有帮助。)
    • 是的,这是一个指示性伪代码。块的跨越是我从未考虑过的。接得好!我将需要添加单词长度并重新阅读我认为的块(取决于单词的大小)
    • 比这更微妙。采用可能具有重叠匹配的模式,例如“教堂”。通常,当您查找“第二个匹配项”时,您会从第一个匹配项结束的位置开始,因此“教堂教堂”中“教堂”的第二个匹配项将是第二个单词。现在如果第一个块在churchur 之后结束会发生什么?第二个块提前五个字符重新启动,因此它看到rchurch church,它记录为两个匹配项,除了第一个块末尾的一个匹配项。 (同样的事情发生在反向,但正向更容易描述。)
    • 是的,这将是一个复杂的方法。 KMP 有一种预处理搜索字符串并到达第一个可能的新位置的方法。我们可以稍微修改和重用该行为。此外,这个问题在任何基于块的处理中都是固有的,无论是向前还是向后。向前说搜索字符串可能是 aaaa,而文件可能只有 a,然后我们必须将块移动到 10MB - strlen(aaaa) + 1(根据 KMP 递增)然后继续。跨度>
    【解决方案3】:

    添加@AlexeyFrunze 的评论,请参阅此处的相关帖子:read file backwards (last line first)。然而,也许面试官对一个解决方案感兴趣,您可以在其中正常向前阅读,看看您将如何解决记忆力有限的问题。

    @ring0 的精彩帖子,所以我只提一些关于从末尾找到 第 k 个单词的问题,其中 k 很小,比如 10,在一个真正文件中,这表明您不应该记住整个文件然后向后搜索。

    您可以维护一个大小为 k 的先进先出缓冲区(也称为 queue),您可以在其中存储遇到匹配项时的位置。随着您找到越来越多的匹配项,您将忘记之前的匹配项。鉴于const int k = 10;long match_pos[k]; 初始化为零,count,您可以寻址match_pos[count % k] = pos 进行排队。到达文件末尾后,

    if (count >= k)
    {
        int kth_match_pos = match_pos[(count + 1) % k];
        // ...
    }
    

    检查缓冲区中最旧的条目,因此您可以跳回 n 字节,其中 npos - kth_match_pos。如果相关上下文也存储在队列中,则无需查找。

    【讨论】:

    • 这很糟糕,因为您将阅读整个文件,根据原始帖子可能有几 GB。逐行阅读也很糟糕,因为您不知道行的长度并且查找非常昂贵。看看我上面的解决方案。
    • @ArunMK 这不正确。限制自己向前阅读是考虑到面试的情况。您发布的解决方案并不能保证找到第 10 个匹配项。
    • 如果你向后搜索缓冲区,它会找到它。
    • 10MB 有一个循环,它从结尾到开头,但以 10MB 为单位。我猜想给 -1 的人没有正确阅读代码。
    猜你喜欢
    • 2014-08-18
    • 1970-01-01
    • 2023-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-11
    相关资源
    最近更新 更多