对几乎排序的数组排序(元素错位不超过k)


69

最近有人问我这个面试问题:

您将得到一个几乎已排序的数组,因为每个N元素的错位可能不超过k正确排序顺序的位置。寻找一种时空高效的算法对数组进行排序。

我有O(N log k)如下解决方案。

让我们来表示arr[0..n)从索引0(包括)到N(不包括)的数组元素。

  • 分类 arr[0..2k)
    • 现在我们知道arr[0..k)它们处于最终排序位置...
    • ...但是arr[k..2k)可能仍然被放错位置k
  • 分类 arr[k..3k)
    • 现在我们知道arr[k..2k)它们处于最终排序位置...
    • ...但arr[2k..3k)可能仍会被k
  • 分类 arr[2k..4k)
  • ....
  • 直到您进行排序arr[ik..N),然后您就完成了!
    • 当您2k剩余的元素少于时,此最后一步可能会比其他步骤便宜

在每个步骤中,您最多要对中的2k元素进行排序,请O(k log k)至少k在每个步骤的末尾将元素置于其最终排序位置。有O(N/k)步骤,因此总体复杂度为O(N log k)

我的问题是:

  • O(N log k)最优的吗?这可以改善吗?
  • 您能做到这一点而无需(部分)重新排序相同的元素吗?

9
我想知道您是否无法利用在步骤1之后[k..2k)相对于彼此排序的事实?因此,您可以对[2k..4k)进行排序,而不是对[k..3k)进行排序,然后将第一个([k..2k))的后半部分与第二个的前半部分([2k..3k)合并)。
Phil

1
是的,这是最佳选择。简单证明,它满足下界是我们随机置换k个元素的每个块:(k)(k)(k)(k)。因此,我们需要对每个取k * log(k)的N / k进行排序。我们可以不借助要素而做到吗?是。如上所述,对k个元素的每个块进行独立排序。然后依次遍历并将块i与块i + 1合并。除了边界外,我们还可以并行并行地进行合并。谢谢你的问题。此算法实际上对于我正在研究的问题很有用:)
Chad Brewbaker

2
@乍得:实际上,莫伦给出了一个更简单的证明:考虑k = n的可能性。然后,比O(n log k)快的算法将与O(n log n)算法用于基于比较的排序的已知最优性相矛盾。
j_random_hacker 2010年

Answers:


37

正如鲍勃·塞奇威克Bob Sedgewick)在其论文工作(及其后续文章)中所展示的那样,插入排序绝对可以压碎“几乎排序的数组”。在这种情况下,您的渐近看起来不错,但如果k <12,我敢打赌,每次插入排序都会获胜。我不知道为什么插入排序效果如此好,但是有一个很好的解释,但是查找的地方应该在Sedgewick的一本教科书《算法》中(他为不同的语言编写了许多版本)。

  • 我不知道O(N log k)是否最优,但更重要的是,我一点也不在乎-如果k小,那么这就是常数因素,如果k大,那么您也可以对数组进行排序。

  • 插入排序将解决此问题,而无需重新排序相同的元素。

对于算法类来说,Big-O表示法非常好,但是在现实世界中,常量很重要。忽视这一点太容易了。(我说这是一位教授Big-O表示法的教授!)


6
您能解释他所说的更多内容而不仅仅是链接到它吗?答案中的参考文献很棒,但是关于stackoverflow本身的实质性内容甚至很棒!
polygenelubricants 2010年

5
好吧,即使在现实世界中,当输入大小增长到足够大时,渐近性也比常量更重要。:-)插入排序具有很好的常数,但是O(n log k)渐近优于O(nk)的事实可能很重要-例如,如果随着n增大k≈√n怎么办?(这还取决于访问者的要求。:p)
ShreevatsaR 2010年

4
@诺曼:也许您实际上可以指向论文/书籍一章,该章对几乎排序的数组有主张?仅链接到主页实际上是没有用的。另外,仅说插入排序将使它无效,例如,如果k = sqrt(n)。我真的不明白为什么这个答案有这么多票。

3
@Moron:如果k = log n,则k小。100万的对数基数2仅为20。@Everyone:SO是一个编程站点,而不是CS理论站点!
诺曼·拉姆齐

5
虽然SO是一个编程站点,但我认为问题仍然值得正确答案。例如,即使在编程中遇到的所有运行时间都以一个常量为界(例如10 ^ 1000),我们也不应该说所有算法都是O(1)。更重要的是,无论插入排序的常量如何,都存在足够大的k,之后插入排序不再更快,并且我们不能“同样”对整个数组进行排序。(我真的怀疑,即使使用一万亿个元素(k = 40),插入排序是否也更快。)
ShreevatsaR 2010年

19

如果仅使用比较模型,则O(n log k)是最佳的。考虑k = n的情况。

要回答您的另一个问题,是的,可以通过使用堆来执行此操作而无需排序。

使用2k元素的最小堆。首先插入2k个元素,然后删除最小值,再插入下一个元素,依此类推。

这样可以保证O(n log k)时间和O(k)空间,并且堆通常具有足够小的隐藏常量。


+1。我还提出了最小堆方法(不能将大小限制为k而不是2k吗?),并被告知对其进行改进,以使其不占用额外的空间。
polygenelubricants 2010年

2
@polygenelubricants:您可以就地执行此操作。从远端开始,并使用最大堆而不是最小堆。在适当位置堆放最后一个2k元素块。将第一个提取的元素存储在变量中;随后的元素进入紧接最后2k块(包含堆结构)之前腾出的位置,类似于常规堆排序。当仅剩下1个块时,将其堆放到位。需要最后的O(n)传递才能将最终块“旋转”回初始块。旋转并非无关紧要,但可以在O(n)和O(1)空间中完成。
j_random_hacker 2010年

@polygenelubricants:奇怪,我似乎错过了您对此答案的评论!@j_random_hacker:似乎正确。

顺便说一句@Moron,我真的很喜欢您的论点,认为O(n log k)是最优的:“考虑k = n”。没有比这更简单的了!
j_random_hacker 2010年

2
@j_random_hacker您能解释一下为什么堆的大小必须为2k吗?在示例中,我完成的k + 1足够大。
JohnS 2013年

8

已经指出,一种渐近最优的解决方案使用最小堆,我只想提供Java代码:

public void sortNearlySorted(int[] nums, int k) {
  PriorityQueue<Integer> minHeap = new PriorityQueue<>();
  for (int i = 0; i < k; i++) {
    minHeap.add(nums[i]);
  }

  for (int i = 0; i < nums.length; i++) {
    if (i + k < nums.length) {
      minHeap.add(nums[i + k]);
    }
    nums[i] = minHeap.remove();
  }
}

3
尽可能添加评论。轻松理解代码是一种好习惯。
roottraveller

7

由于k显然应该很小,所以插入排序可能是最明显且最普遍接受的算法。

在对随机元素进行插入排序时,您必须扫描N个元素,并且每个元素必须平均移动N / 2个位置,从而使总操作量达到〜N * N / 2。“ / 2”常数在big-O(或类似)表征中被忽略,从而导致O(N 2)复杂性。

在您提议的情况下,预期的运算数为〜N * K / 2,但是由于它k是一个常数,因此k/2在big-O表征中会忽略整个项,因此总体复杂度为O(N)。


2
k不能保证一定是常数,所以这是真的O(Nk)k但是,如果是一个常数,那是正确的O(N)
polygenelubricants 2010年

7

如果k足够大,您的解决方案就是一个很好的解决方案。就时间复杂度而言,没有更好的解决方案。每个元素都可能错位放置k,这意味着您需要学习log2 k一些信息才能正确放置它,这意味着您log2 k至少需要进行比较-因此,它至少必须具有一定的复杂性O(N log k)

但是,正如其他人所指出的那样,k常数很小会杀死您。在这种情况下,请使用每次操作都非常快的操作,例如插入排序。

如果您确实想获得最佳效果,则可以实现这两种方法,然后根据进行选择k

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.