对一个 n 元素数组进行排序,使前 k 个元素按升序排列最低(就地算法)

Posted

技术标签:

【中文标题】对一个 n 元素数组进行排序,使前 k 个元素按升序排列最低(就地算法)【英文标题】:Sort an n-element array so first k-elements are lowest in increasing order (In place algorithm) 【发布时间】:2014-06-15 03:35:04 【问题描述】:

这是一个我一直坚持的家庭作业问题。

我需要对一个 n 元素数组进行排序,以便前 k 个元素是最低的并且按升序排列。对于 k

我的解决方案: 我想到的一个简单的解决方案是堆化(O(n))数组。然后删除 k 元素并将堆/数组的起始索引从 0 移动到 1 - 2 - 3(依此类推,一直到 k)。这将是 O(n+k*lg(n)+k*n) = O(kn+k*lg(n))。对于 k 的给定条件,它将是 O(n^2/log(n) + n)。

另一种可能的实现是使用基数排序,这将是 O(n),但我觉得这不是正确的解决方案,因为我将对整个数组进行排序,而他们只要求对 k 个元素进行排序。

你不必给我答案,只是提示会有所帮助。

【问题讨论】:

Quickselect 算法。 C++ std::partial_sort() @timrau 有。使用 O(n) 算法对第 k 个最大的进行分区,然后对第 k 个进行排序。这是 O(n + k log k) 和常数空间。 @timrau:你或 Gene 应该把它写下来作为答案。渐近地,这是最好的解决方案,根据我的经验,QuickSelect 比就地 BuildHeap 快得多。 基数排序可以工作,只要你的数字范围不大于数组的大小。我看不出对整个数组进行排序会有什么问题。您执行此操作的任何方式都有可能修改整个数组。那么如果修改导致整个数组被排序呢? 【参考方案1】:

我喜欢你的堆想法。我实际上认为它会在您列出的时间范围内起作用,并且您的分析中存在一些小故障。

假设您执行以下操作:在您的数组中构建一个就地堆,然后将最少 k 个元素出列,将剩余的 n - k 个元素留在数组中它们所在的位置。如果您考虑元素将在哪里结束,您应该将数组中的 k 个最小元素按升序存储在数组的后面,而剩余的 n - k 个元素将按堆顺序存储在前面。如果您在看到这一点时遇到困难,请考虑堆排序的工作原理 - 在 k 个出队之后,最大的 k 个元素在后面按降序排列,其余元素在前面按堆排序。在这里,我们将最小堆换成了最大堆,因此出现了奇怪的排序。因此,如果你在最后反转数组,你应该在前面有 k 个最小元素以升序排列,然后是 n - k 个剩余元素。

这样会正确找到k个最小的元素,运行时间确定如下:

heapify 的成本:O(n) k 个出队的成本:O(k log n) 反转数组的成本:O(n) 总成本:O(n + k log n)

现在,假设 k ≤ n / log n。那么运行时是

O(n + k log n) = O(n + (n / log n) log n) = O(n)

所以你完成了!该算法工作得很好。另外,它需要 O(1) 辅助空间(堆可以就地构建,并且可以在 O(1) 空间中反转数组)。

不过,你可以做得更好。 @timrau 在 cmets 中建议您使用快速选择(或更一般地说,任何线性时间选择算法)。这些算法重新排列数组,以某种顺序将最低 k 个元素放在数组的前 k 个槽中,并将剩余的 n - k 个元素按某种顺序放在最后 n - k 个槽中。这样做需要时间 O(n) 而不管 k (漂亮!)。所以假设你这样做,然后只对前 k 个元素进行排序。这需要时间 O(n + k log k),渐近优于 O(n + k log n) 时间的基于堆的算法。

在已知的线性时间选择算法中,如果你小心的话,快速选择和中位数算法都可以就地实现,因此这种方法所需的总空间是 O(1)。

【讨论】:

我在这里遗漏了一些东西。完成后,数组的前k个位置有最少k个元素吗? @JimMischel 我相信如果您将所有内容隐含地表示为堆,就会发生这种情况。不过,根据您的操作方式,您可能需要在最后反转数组。我正在考虑堆排序的标准实现,它使用堆的就地表示来将所有内容放在正确的位置。我错过了什么吗? (另外,我还能做些什么来改进这个答案?) 通常,如果要对数组进行升序排序,则构建一个最大堆,然后从后到前构建排序后的数组。对于这个问题,您可能希望构建一个最小堆 backwards: 并在数组末尾使用最小项。然后执行您的 k SelectMin 操作并将所选项目放在数组的开头。如果不清楚,请让我详细说明。我只是重新阅读它并意识到它假设了一些上下文。 @JimMischel 这很有道理。如果我更新我的答案以添加该详细信息,这足以让您删除反对票吗?或者您还希望我提供其他详细信息吗? 我不是反对者。但是,涉及反转数组的更改足以使答案足够有用,值得投票。【参考方案2】:

我突然想到,您可以使用稍微修改的堆选择算法就地执行此操作,即 O(n log k)。尽管比 Quickselect 的 O(n) 复杂度渐近“更糟”,但当 k 与 n 相比非常小时,堆选择可以胜过 Quickselect。有关详细信息,请参阅When theory meets practice。但是,如果您要从一百万个列表中选择前 1000 个项目,那么堆选择几乎肯定会更快。

无论如何,要做到这一点,您可以从数组中的前 k 个项目在数组的前面构建一个大小为 k 的最大堆(使用标准 BuildHeap 函数)。这需要 O(k)。然后,您像这样处理数组中的其余项目:

for (i = k; i < length; ++i)

    if (array[i] < array[0])  // If item is smaller than largest item on heap
    
        // put large item at the current position
        temp = array[i];
        array[i] = array[0];

        // put new item at the top of heap and sift it down
        array[0] = temp;
        SiftDown(0);
    

这将花费 O(n log k) 时间,但限制因素实际上是您必须在条件内执行多少次代码。只有当一个项目小于堆上已经存在的最大项目时,此步骤才会进行任何处理。最坏的情况是当数组处于反向排序顺序时。否则它会出奇的快。

完成后,最小的 k 项位于数组的前面。

然后您必须对它们进行排序,即 O(k log k)。

所以完整的过程是 O(k + n log k + k log k)。同样,当 k 远小于 n 时,这比 Quickselect 快得多。

【讨论】:

投反对票?习惯上留下评论来解释您发现答案的错误之处。

以上是关于对一个 n 元素数组进行排序,使前 k 个元素按升序排列最低(就地算法)的主要内容,如果未能解决你的问题,请参考以下文章

在 n 个元素的数组中,首先对 n-(root)n 个元素进行排序,我们要对数组进行排序

如何查找无序数组中的Top n

分而治之——未排序数组的k个元素

想在含有n个元素的序列中得到最小的前k个元素,最好采用啥排序算法

寻找数组中的第K大的元素,多种解法以及分析

未排序长度 n 数组中 k 个最大元素的索引