我最近被问到这个面试问题:
您将获得一个几乎已排序的数组,因为每个
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)
最佳吗?这可以改进吗?答案 0 :(得分:36)
正如Bob Sedgewick在他的论文工作(和后续工作)中所示,插入排序绝对粉碎“几乎排序的数组”。在这种情况下,你的渐近线看起来很好但是如果k < 12我打赌插入排序每次都获胜。我不知道为为什么插入排序做得很好有一个很好的解释,但是看看的地方将是Sedgewick的教科书中的一个名为算法(他已经完成了许多不同语言版本。)
我不知道O(N log k)是否是最优的,但更重要的是,我并不在乎 - 如果k很小,那么重要的是常数因子,如果k很大你也可以对数组进行排序。
插入排序会解决此问题,而无需重新排序相同的元素。
Big-O表示法对于算法类来说非常好,但在现实世界中,常量很重要。很容易忽视这一点。 (我说这是教授Big-O符号的教授!)
答案 1 :(得分:19)
如果仅使用比较模型,则O(n log k)是最佳的。考虑k = n时的情况。
要回答您的其他问题,是的,可以通过使用堆来进行排序。
使用2k元素的最小堆。首先插入2k元素,然后删除min,插入下一个元素等。
这保证了O(n log k)时间和O(k)空间,而堆通常具有足够小的隐藏常量。
答案 2 :(得分:7)
如果k
足够大,那么您的解决方案很好。在时间复杂性方面没有更好的解决方案; k
个地方的每个元素可能不合适,这意味着您需要学习log2 k
个信息才能正确放置,这意味着您至少需要进行log2 k
次比较 - 所以它必须是至少O(N log k)
的复杂性。
然而,正如其他人所指出的那样,如果k
很小,那么常数术语就会杀了你。在这种情况下,使用每次操作非常快的东西,比如插入排序。
如果你真的想要达到最佳状态,你可以实现这两种方法,并根据k
从一种方法切换到另一种方法。
答案 3 :(得分:7)
已经指出,其中一个渐近最优解决方案使用最小堆,我只是想用Java提供代码:
body{
background-color: whitesmoke;
background-image: url("https://static.pexels.com/photos/1562/italian-landscape-mountains-nature.jpg");
background-repeat: no-repeat;
background-size: 1400px 750px;}
答案 4 :(得分:6)
由于k
显然应该非常小,因此插入排序可能是最明显且普遍接受的算法。
在随机元素的插入排序中,您必须扫描N个元素,并且必须将每个元素平均移动N / 2个位置,从而总共运算~N * N / 2个。在大O(或类似)特征中忽略“/ 2”常数,给出O(N 2 )复杂度。
在您提议的情况下,预期的操作次数为~N * K / 2 - 但由于k
是常量,因此整个k/2
项将被忽略。 O表征,因此总体复杂度为O(N)。