【问题标题】:Fastest method for running a binary search on a file in C?在 C 中对文件运行二进制搜索的最快方法?
【发布时间】:2025-12-21 17:10:06
【问题描述】:

例如,假设我想在文件中查找特定的单词或数字。内容按排序顺序(显然)。由于我想对文件运行二进制搜索,因此将整个文件复制到数组中然后运行二进制搜索似乎真的是浪费时间......我已经有效地将其设为线性时间算法,因为我'在运行搜索之前,我必须花费 O(n) 时间复制该死的文件。

有没有更快的方法来做到这一点?是否有类似 lseek 的东西可以使用行而不是字节?

如果没有,我是否最好只进行线性搜索(假设我在整个程序期间只运行搜索一次)?

【问题讨论】:

    标签: c binary-search


    【解决方案1】:

    您不能逐行查找。仔细想想就很明显了。

    但是您可以对文本文件进行某种二进制搜索。

    你要做的是:

    • 统计文件以获取长度或搜索到末尾并获取位置。
    • 内存映射文件。
      (我认为这是最好的,但如果需要,您可以使用 lseek 和阅读。)
    • 寻找到文件的中间,减去你的平均行长。只是猜测。
    • 向前扫描换行符,除非您在位置 0。
    • 阅读您的台词并进行比较。
    • 重复 1/4 或 3/4、1/8、1/16 等。

    【讨论】:

    • “减去平均行长”位并不是真正需要的。请注意栅栏错误。
    • 这是一个不错的答案,当然,假设您的文件适合您的地址空间。 + 1.
    • 内存映射文件;它减少了扫描内存块中没有可见 I/O 的行的问题。
    • 我想不是,但我最后一次这样做(不久前)似乎更可靠地获得了中间字符串。出于某种原因,这在当时似乎很重要。呵呵。
    • 它只在页面被访问时将它们复制到内存中——请求分页。这与使用 read 时发生的情况没有什么不同(实际上,它更好,因为它涉及的内存-内存副本更少)。
    【解决方案2】:

    基于磁盘的二分搜索至少在开始时需要“块感知”,即知道无论您读取整组数据中的单个字节,I/ O 成本是一样的。另一个需要注意的是与顺序读取操作相比,查找操作的成本相对较高

    它可以使用这种对磁盘 I/O 特性的认识的几种方式:

    • 在搜索接近尾声时,倾向于线性搜索(扫描)而不是搜索。
    • 在开始时检查块中的第一个和最后一个元素,这可能有助于推断下一个拆分的更好猜测
    • 缓存文件中不同位置的一些项目的树(甚至是简短的平面列表)(有点像正式 btree 结构中的中间节点)
    • 声明并使用适当的缓冲区大小

    【讨论】:

      【解决方案3】:

      如果文件很小,例如不到几百 KB,那么将整个文件读取(或虚拟内存映射)到内存中几乎肯定会更快。这是因为执行多个 i/o 操作来查找和传输的开销比仅读取整个文件要糟糕得多,这是大多数程序所做的并且大多数操作系统都假定已完成。

      除非所有行的长度都相同,或者具有非常可预测的长度,否则很难找到第 #n 行。但是,为了执行二分搜索,我会在二分搜索中使用字节偏移量,并在偏移量之前和之后读取 100 个字节(如果单词的长度都小于 100 个字符)——总共 200 个字节。然后扫描它中间前后的换行符以提取单词。

      【讨论】:

        【解决方案4】:

        是的,您可以 lseek,但如果每行每个单词/数字的大小是固定的,这将有所帮助,如果不是这种情况,则更有可能,那么您必须按文件大小查找并查找最近的词开始仍然接近二进制搜索的典型 O(log n) 时间复杂度。

        【讨论】:

          【解决方案5】:

          不会有“lseek”函数,因为文件命令没有“行”的概念。这个概念存在于与原始文件命令不同的抽象层中。

          至于它是否更快,答案将取决于许多因素,包括文件大小、磁盘驱动器速度和可用 RAM 量。如果它不是一个大文件,我猜将整个文件加载到内存中会更快。

          如果它是一个大文件,我会使用二进制搜索算法将其缩小到更小的范围(例如,几兆字节),然后加载整个块。

          【讨论】:

            【解决方案6】:

            如上所述,由于文件是文本文件,因此无法可靠地预测文件中给定行开始的字节。 ersatz 二进制搜索的想法是一个非常好的想法。但考虑到现在顺序 I/O 的速度有多快以及随机 I/O 的速度有多慢,除非文件很大,否则它真的不会为您节省很多。

            正如您所提到的,如果您要阅读它,您不妨边走边线性搜索它。所以这样做,在阅读时使用修改后的 Boyer-Moore 搜索,你会做得很好。

            【讨论】:

            • Boyer-Moore 通常是明智之举,但在这种情况下,我每行只有一个单词。所以 strcmp() 在这种情况下将具有与 Boyer-Moore 相同的运行时间。
            【解决方案7】:

            这里有如此多的性能权衡,以至于在您对典型数据进行测量之前,不可能知道什么是有意义的。

            如果您要维护此代码,它需要简单。如果搜索很少或文件很小,请使用线性搜索。如果成本真的很重要,您将不得不做一些实验。

            在线性搜索之后我会尝试的第二件事是mmap 文件并扫描它以查找换行符。这确实需要线性时间,但strchr 可以非常快。如果您可以保证文件以换行符结尾,这会有所帮助。划定界限后,您可以通过二分查找来减少比较次数。

            您应该考虑的另一个选项是 Boyer-Moore 字符串搜索。这是一种亚线性时间搜索,根据搜索模式的大小,它可能比对数二分搜索更快。 Boyer-Moore 特别擅长处理长搜索字符串。

            最后,如果您确定二进制搜索确实很好,但识别行是性能瓶颈,您可以预先计算每行的起始位置,并将这些预先计算的位置以二进制格式存储在辅助文件中。

            我觉得只做一个预测很舒服:几乎可以肯定的是,避免一次读一行像 readline()fgets() 这样的东西是值得的,因为这种策略总是涉及调用 malloc() 来保存线。在每一行上调用malloc() 的成本可能会超过任何搜索或比较的成本。

            【讨论】:

            • 我一直和你在一起直到最后一行......你的意思是“几乎肯定值得避免在每一行调用 malloc() 的成本。” ?
            • 不是很清楚。我不明白 为什么 你会为每一行调用 malloc。我从来没有那样做 fgets,我一直使用静态字符缓冲区。
            • 您可以调用 malloc() 以便可以读取任意大小的行,而不是受限于静态字符缓冲区的大小。
            • 好吧,我想我可以看到在需要时使用 realloc 来获得更大的缓冲区,但没有必要继续释放它。那将是一个主要是静态的缓冲区。
            • 如果为了对它们进行二分搜索而保留这些行,则必须将每一行保存在堆上。否则,当您准备进行二进制搜索时,只有最后一行会在缓冲区中。为某事节省空间的方法当然是malloc()