【问题标题】:Given 1 billion numbers we need to find the largest 1 million numbers给定 10 亿个数字,我们需要找到最大的 100 万个数字
【发布时间】:2020-08-14 23:30:09
【问题描述】:

我被一个问题困住了。

给定 10 亿个数字,我们需要找到最大的 100 万个数字。一种方法是排序 数字,然后从 O(n log n) 中取出前一百万个数字。提出一个算法 预期 O(n) 时间复杂度。

它是堆排序,它可以在 O(n) 复杂度下做到这一点吗?

【问题讨论】:

  • 我回滚了答案,因为似乎人们一致认为新的编辑没有意义,并且现有的答案使用了原始版本。如果还有其他问题的信息大相径庭,您可以考虑在单独的问题中提出。
  • 堆排序不能用 O(n) 做到这一点,它是 O(n log n),就像其他被认为是好的基于比较的排序一样。

标签: algorithm sorting heap


【解决方案1】:

您在此处尝试解决的问题的一般版本似乎如下:

给定 n 个数字,在(可能预期的)时间 O(n) 内报告其中最大的 k 个。

如果您只需要查找前 k 个元素并且排序无关紧要,则可以使用基于快速 selection algorithms 的巧妙 O(n) 时间算法来解决此问题。作为复习,选择算法将数组 A 和数字 m 作为输入,然后对数组 A 重新排序,使 m 个最小的元素位于前 m 个槽中,其余元素占据较大的槽。 quickselect 算法在(预期的)时间 O(m) 内完成此操作,并且在实践中很快; median-of-medians 算法在最坏情况 O(m) 时间内执行此操作,但在实践中速度较慢。虽然这些算法通常以查找 最小 k 个元素为框架,但它们在查找 最大 k 个元素方面同样有效。

使用这个算法作为子程序,下面是我们如何找到时间和空间 O(m) 中前 k 个元素的方法:

Initialize a buffer of 2k elements.
Copy the first k elements of the array into the buffer.

While there are elements remaining in the array:
    Copy the next k of them into the buffer.
    Use a selection algorithm to place the k largest elements
      of the buffer in the first k slots of the buffer.
    Discard the remaining elements of the buffer.

Return the contents of the buffer.

要了解为什么会这样,请注意,在循环的每次迭代之后,我们保持缓冲区保存迄今为止所见的 k 个最大元素的不变量(尽管不一定按排序顺序)。因此,该算法将识别输入的前 k 个元素并按某种顺序返回它们。

就时间复杂度而言 - 创建缓冲区需要 O(k) 次工作,并且在循环的所有迭代中,我们需要 O(n) 次工作将元素复制到缓冲区中。每次调用选择算法都需要(预期的)时间 O(k),并且有 O(n / k) 次调用该算法的净运行时间为 O(n + k)。在 k

【讨论】:

    【解决方案2】:

    没有一般的排序算法可以在 O(n) 时间内做到这一点。此外,如果没有额外的限制(例如,十亿个数字取自数字 1 到 1,000,000),根本就没有排序算法可以解决这个问题。

    但是,有一个简单的 O(n) 算法可以做到这一点:

    1. 用 1,000,000 个空单元格初始化返回缓冲区
    2. 对于 1,000,000,000 列表中的每个项目,请执行以下操作:
    3. 依次检查 1,000,000 个单元格中的每一个
    4. 如果输入的数字大于缓冲区中的数字,则交换它们并继续
    5. 如果您正在查看空白单元格,请输入您按住的数字
    6. 如果您到达列表末尾并持有一个数字,请将其扔掉

    这是一个包含 10 项内容的示例,我们想要最大的 5 项:

    Input:  [6, 2, 4, 4, 8, 2, 4, 1, 9, 2]
    Buffer: [-, -, -, -, -]
            [6, -, -, -, -] … see a blank, drop the 6
            [6, 2, -, -, -] … 2 < 6, skip, then see a blank, drop the 2
            [6, 4, 2, -, -] … 4 < 6 but 4 > 2, swap out 2, see blank, drop 2
            [6, 4, 4, 2, -] … 4 <= 4,6 but 4 > 2, swap out 2, see blank, drop 2
            [8, 6, 4, 4, 2] … 8 > 6, swap, then swap 6 for 4, etc.
            [8, 6, 4, 4, 2] … 2 <= everything, drop it on the floor
            [8, 6, 4, 4, 4] … 4 <= everything but 2, swap, then drop 2 on floor
            [8, 6, 4, 4, 4] … 1 <= everything, drop it on the floor
            [9, 8, 6, 4, 4] … 9 > everything, swap with 8 then drop a 4 on the floor
            [9, 8, 6, 4, 4] … 2 <= everything, drop it on the floor
    

    您对输入中的每个元素进行 1,000,000 次比较,并可能进行多达 1,000,000 次交换(考虑按升序排序的输入)。这意味着您所做的工作与 1,000,000 * n 成正比,这是输入 n 大小的线性工作量。

    【讨论】:

    • 不知道你为什么叫这个O(n) - 这看起来更像是一个插入排序。
    • 与其保持 1M 单元格有序,不如将它们组成一个最小堆。不需要完全排序,您只需将新候选者与堆的最小值进行比较,如果候选者较大,则将其弹出,然后推送新值。
    • @SomeDude 是 O(n),常数因子约为 100 万。这是一个很大的常数因子,当然,但它是常数。
    • @SomeDude 可能更接近 1M x 1B(减去一点,因为检查的前 1M 数字将进入某个空白单元格)。
    • @templatetypedef 我想我可以回滚最后的编辑。原始版本是有道理的,即使这是一个有点奇怪的问题。
    【解决方案3】:

    您可以一般做得比排序更好。大多数人会通过使用堆作为结构来解决这个问题。构建堆的时间复杂度为 O(n)。但是您将不得不执行一百万次“弹出”操作,每个弹出的时间复杂度为 O(log n),但您并没有执行完整的 n 次弹出(在这种情况下只有 n/1000 次弹出)。

    我说你“通常”可以比排序做得更好,因为库中的大多数排序算法都是 O(n log n)。但是有一个实际上是 O(n + k) 的“分布排序”,其中 k 是您正在排序的范围内可能值的数量,并且根据 k 的值,您可能会这样做更好的排序。

    更新

    为了结合@pjs 的建议,创建一个“最小”堆,其中包含十亿中的前一百万个值,其中弹出操作会从堆中删除最小值。然后对于接下来的 999,000,000 个值,检查每个值是否大于堆上的当前最小值,如果是,则从堆中弹出当前最小值并推送新值。完成后,将留下 1,000,000 个最大值。

    【讨论】:

    • 不要创建包含十亿的堆,存储一百万,然后在十亿中的下一个候选者大于当前最小值时替换最小值。当你走到尽头时,堆中包含你最大的一百万。
    • 平均而言,1000 个值中只有 1 个最终会被推送到较小的堆上。您只需跳过其他人即可。
    • 如果您被允许修改输入数组(就像您对快速选择所做的那样),您可以就地执行此操作。只需将数组中的前 100 万个项目重新排列为堆顺序即可。然后对其余项目进行标准堆选择。
    • 或者只是编写自己的部分堆实现。您所需要的只是构建堆和筛选。
    • 最坏情况下的输入将是一个升序数组,因为每个项目都会导致替换根,并且它会被筛选到堆的最底层。该算法的复杂度为 O(n log k),每 n 个都需要 log(k) 级移动。
    猜你喜欢
    • 1970-01-01
    • 2015-12-09
    • 2011-07-15
    • 2012-01-17
    • 2018-09-27
    • 2011-01-29
    • 2011-02-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多