在插入项目或将它们添加到已排序列表之后对列表进行排序是否更快?


72

如果我有一个排序列表(比如说要进行排序的快速排序),如果我要添加很多值,那么最好是暂停排序,然后将它们添加到末尾,然后进行排序,或者使用二进制印章正确地放置项目添加它们。如果项目是随机的,或者已经或多或少地按顺序排列,会有所不同吗?


它是数组还是链表?我知道您说的是“列表”,但您提到过进行二进制切分,这意味着一个数组。

将标签“算法”更改为“算法”
Eric

Answers:


37

如果添加了足够多的项以使您可以从头开始有效地构建列表,则应通过对列表进行排序来获得更好的性能。

如果项目大部分是按顺序排列的,则可以调整增量更新和常规排序以利用这一点,但是坦率地说,通常不值得这样做。(您还需要注意一些事情,例如确保某些意外排序不会使您的算法花费更长的时间,qv天真的quicksort)

增量更新和常规列表排序都为O(N log N),但是您可以在以后进行所有排序时得到一个更好的恒定因子(我在这里假设您已经有了一些辅助数据结构,因此您的增量更新可以比O更快地访问列表项(N)...)。一般而言,一次进行全部排序比增量维护顺序具有更多的设计自由度,因为增量更新必须始终保持完整的顺序,而一次全部批量排序则不需要。

如果没有别的,请记住,有很多高度优化的批量排序可用。


21

通常,使用要好得多。简而言之,它在推动器和选择器之间分配了维护订单的成本。像大多数其他解决方案一样,这两个操作都是O(log n),而不是O(n log n)。


5
如果列表是某种优先级队列,则这是特别好的建议。谷歌在这种情况下弱堆。
Daniel Rikowski

10

如果要成束添加,则可以使用合并排序。对要添加的项目列表进行排序,然后从两个列表中进行复制,比较项目以确定下一个要复制的项目。如果调整目标数组的大小并从头到尾进行工作,您甚至可以就地复制。

此解决方案的效率为O(n + m)+ O(m log m),其中n是原始列表的大小,m是要插入的项目数。

编辑:由于这个答案没有得到任何爱,我想我会用一些C ++示例代码充实它。我假设排序后的列表保存在链接列表中,而不是数组中。这使算法看起来更像是插入而不是合并,但是原理是相同的。

// Note that itemstoadd is modified as a side effect of this function
template<typename T>
void AddToSortedList(std::list<T> & sortedlist, std::vector<T> & itemstoadd)
{
    std::sort(itemstoadd.begin(), itemstoadd.end());
    std::list<T>::iterator listposition = sortedlist.begin();
    std::vector<T>::iterator nextnewitem = itemstoadd.begin();
    while ((listposition != sortedlist.end()) || (nextnewitem != itemstoadd.end()))
    {
        if ((listposition == sortedlist.end()) || (*nextnewitem < *listposition))
            sortedlist.insert(listposition, *nextnewitem++);
        else
            ++listposition;
    }
}

O(n + m)+ O(m log m)为O(n + m)
Miles Rout

3
@MilesRout,完全不正确。m log m > m因此,最好的方法就是简化它O(n+(m log m))
Mark Ransom

糟糕,在记录m之前没有看到m。不理我!
Miles Rout

4

原则上,创建树比对列表进行排序要快。对于每个插入,树插入均为O(log(n)),从而得出总体O(n log(n))。排序为O(n log(n))。

这就是Java具有TreeMap的原因(除了List的TreeSet,TreeList,ArrayList和LinkedList实现之外)。

  • TreeSet使事物保持对象比较顺序。密钥由Comparable接口定义。

  • LinkedList使事物保持插入顺序。

  • ArrayList使用更多的内存,对于某些操作来说更快。

  • 同样,TreeMap消除了按键排序的需要。映射在插入过程中按键顺序构建,并始终按排序顺序进行维护。

但是,由于某种原因,TreeSet的Java实现比使用ArrayList和sort慢得多。

[很难推测为什么它会显着变慢,但是确实如此。一遍遍数据应该稍微快一点。这种事情通常是内存管理的成本超过算法分析的成本。]


2
我会小心地说一棵树比一棵树快。它实际上取决于输入的大小和所使用的树实现。
hazzen

2
运行一些速度测试,您会发现情况并非如此。TreeSet与ArrayList相比,将500k个随机数添加,排序并将其转储到另一个列表中,ArrayList的速度快约2倍。如果我们不将它们转储到另一个列表,则ArrayList获胜约1.6倍。
08年

TreeSet和TreeMap本质上是同一类。TreeSet <E>是TreeMap <E,Object>,其值在插入时设置为单例对象。时间几乎相同,但仍比ArrayList解决方案慢约2倍。
08年

我说过,将全部插入ArrayList + Collections.sort的速度比仅将全部插入Tree [Set | Map]快约2倍。这是用于大量值。对于少量的值,该差异仍然约为2倍,但是1ms与2ms并不重要。
08年

速度差异的原因是ArrayList是使用单个数组实现的,而树形图是一个链接结构,每个条目具有不同的节点对象。访问数组要快得多,并且JVM可以比对象优化(重用寄存器,更好的缓存局部性)
ddimitrov

3

我会说,让我们测试一下!:)

我尝试过使用quicksort,但是用quicksort对几乎排序的数组进行排序是……好吧,不是一个好主意。我尝试了一种修改的方法,该方法减少了7个元素,并为此使用了插入排序。仍然,可怕的表现。我切换到合并排序。它可能需要很多内存来进行排序(这不是就地执行的),但是在排序数组上的性能要好得多,而在随机数组上的性能几乎是相同的(两种方法的初始排序几乎都花了相同的时间,quicksort的速度稍快一些) )。

这已经表明一件事:问题的答案在很大程度上取决于您使用的排序算法。如果在几乎已排序的列表上性能不佳,那么在正确位置插入将比在末尾添加然后重新排序要快得多;合并排序可能不是您的选择,因为如果列表很大,可能需要太多外部存储器。顺便说一句,我使用了一个自定义的合并排序实现,它仅将1/2的外部存储空间用于朴素的实现(需要与数组大小本身一样多的外部存储空间)。

如果肯定不能选择合并排序,而不能确定快速排序,那么最好的选择可能是堆排序。

我的结果是:仅在最后添加新元素,然后对数组重新排序比在正确位置插入它们快几个数量级。但是,我的初始数组有10个mio元素(已排序),而我正在添加另一个mio(未排序)。因此,如果将10个元素添加到10个mio的数组中,则正确地插入它们比对所有元素重新排序要快得多。因此,您问题的答案还取决于初始(排序)数组的大小以及要添加到其中的新元素的数量。


1

如果列表是a)已经排序的列表,并且b)本质上是动态的,则插入排序列表中的列表应该总是更快(找到正确的位置(O(n))并插入(O(1)))。

但是,如果列表是静态的,则必须进行列表其余部分的重排(O(n)可以找到正确的位置,O(n)可以将内容滑落)。

无论哪种方式,插入排序列表(或类似二叉搜索树)的速度都应该更快。

O(n)+ O(n)应始终比O(N log n)快。


动态构造(例如链表)中的插入对于每个insert仍为O(1)。所以是的,总的来说,它们的总和为O(N)-但它不是乘法的,而是加法的(即2乘以O(n),而不是O(n ^ 2))。
沃伦

如果操作正确且数据分布相对均匀,则插入应为O(log(N))
12:56窃听

您的第一段描述了两个排序链表的单个合并。如果一个合并为O(N),则您的总排序将为O(NlogN),除非您以某种方式可以在不到O(NlogN)的时间内获得O(1)个排序的块。通过将每个元素插入二叉搜索树进行增量排序是O(N log N),因为插入操作是O(logN),您必须进行N次。(简单的二叉树有一个元素的O(N)最坏情况插入。)无论如何,最后两段都是胡说八道。这些都不能帮助您击败O(NlogN),甚至无法击败qsort。
彼得·科德斯

@ PeterCordes-我根本没有描述两个排序列表的合并:我描述的是将未知排序顺序的项目添加到已经排序的列表中
沃伦(

1

差不多。将项目插入到已排序的列表中是O(log N),对列表中的每个元素N进行此操作(因此构建列表)将是O(N log N),这是快速排序(或合并排序)的速度更接近这种方法)。

如果您将它们插入到前面,它将是O(1),但是在后面进行快速排序时,它仍然是O(N log N)。

我会采用第一种方法,因为它可能会稍快一些。如果列表的初始大小N远大于要插入的元素数X,则插入方法为O(X log N)。插入列表的开头后的排序为O(N log N)。如果N = 0(即:您的列表最初为空),则按排序顺序插入或之后进行排序的速度相同。


不必挑剔,但是N是要插入的元素数,因此答案的最后一段对我来说没有太大意义!您是说“如果N不太大”吗?
Remo.D

编辑以在Remo.D的评论后澄清。
bmdhacks

第2段在某些情况下是错误的。在几乎排序的列表上进行快速排序会接近O(n ^ 2),而不是O(n log n)。
Tony BenBrahim,

1

从高层次看,这是一个非常简单的问题,因为您可以将排序视为只是迭代搜索。当您要将元素插入到有序数组,列表或树中时,必须搜索要插入该元素的点。然后,您可以以低廉的价格将其放入。因此,您可以将排序算法想像成只是处理一堆事情,然后一步一步地寻找合适的位置并将其插入。因此,插入排序(O(n * n))是迭代线性搜索(O(n))。树,堆,合并,基数和快速排序(O(n * log(n)))可以被视为迭代二进制搜索(O(log(n)))。如果基础搜索为有序哈希表中的O(1),则可能具有O(n)排序。(例如,将52张卡片放入52个垃圾箱中,从而对其进行分类。)

因此,您的问题的答案是,从大O角度来看,一次插入一个内容,而不是先保存然后再对它们进行排序应该没有多大区别。当然,您可能需要处理恒定的因素,而这些因素可能很重要。

当然,如果n小,例如10,那么整个讨论都是愚蠢的。



0

(如果您正在谈论的列表像C#一样List<T>。)将一些值添加到具有许多值的排序列表中的正确位置将需要较少的操作。但是,如果要添加的值数量变大,它将需要更多。

我建议不要使用列表,而是使用一些更合适的数据结构。例如,像二叉树。具有最小插入时间的排序数据结构。


0

将项目插入排序列表需要O(n)时间,而不是O(log n)时间。您必须O(log n)花点时间找到放置它的地方。但是随后您必须转移所有要素-需要O(n)时间。因此,在保持排序度的同时进行插入是O(n ^ 2),在插入所有内容然后进行排序的情况下O(n log n)

取决于您的排序实现,O(n log n)如果插入数比列表大小小很多,您可以获得甚至更好的结果。但是,如果是这样的话,无论哪种方式都没有关系。

如果插入的数量很大,那么请全部插入并排序解决方案,否则可能无关紧要。


我认为您对O表示法的看法完全不正确。将项目插入列表不是O(n),在算法定理中始终为O(1)。在内存中移动数百万个字节可能不是一个恒定的操作,但是O表示的不是时间,而是复杂度,即1
Mecki,

如果不是恒定操作,则不是O(1)。期。用于插入列表的代码是(对于基于数组的列表):for(i = last; i> idx; --i){list [i + 1] = list [i]; } list [idx] = item; 我认为您不会争论O(n)。您不能只在Big O中忽略部分代码
。–

1
如果它受任何N的某个常数限制,则为O(1)。有一些方法可以组织数组,从而提高插入效率,例如使之成为具有一定空白空间的块。
Mike Dunlavey,

-1

如果这是.NET并且项目是整数,则将它们添加到Dictionary的速度更快(或者,如果您使用的是.Net 3.0或更高版本,则如果您不介意丢失重复项,则使用HashSet)这可以使您自动进行排序。

我认为字符串也将以相同的方式工作。好处是您可以通过这种方式获得O(1)插入和排序。


3
Dictionary <T>不是排序的集合。SortedDictionary <T>是。
Ihar Bury

-2

将项目插入到已排序的列表中为O(log n),而将列表排序为O(n log N),这表明最好先排序然后再插入

但是,大的'O'只涉及项目数量的速度缩放,这可能是因为对于您的应用程序而言,在中间插入一个插入是昂贵的(例如,如果它是一个向量),因此在后面附加和排序可能会更好。


插入到排序列表中的是O(log n)。插入到哈希中的是O(1)。
bmdhacks

好的,您已经固定了符号,但是现在您的第一条陈述是不正确的。排序和插入的速度相同。排序为O(N log N),插入操作执行N次O(log N),因此为O(N log N)。
bmdhacks

1
但这与N不同,如果您只需要在100万个中插入10个项目,则10 *(log 1M)拍10 +(1M log 1M)ps。抱歉,我给您留下了评论,感谢您发现拼写错误,但它似乎已经消失了吗?
Martin Beckett

很公平。从技术上讲,Big-O不在乎N的大小,只有Big-Omega才在乎,但是只有计算机科学教授才可能在乎。感谢您接受我的审查。
bmdhacks

而且大多数人都认为O()可以告诉您有关速度的所有信息。建造金字塔为O(n),但比对金字塔的高度进行排序要慢得多!
Martin Beckett
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.