添加到集合然后对其进行排序或添加到已排序的集合是否更快?


79

如果我有Map这样的话:

HashMap<Integer, ComparableObject> map;

并且我想获得使用自然顺序排序的值的集合,哪种方法最快?

(一种)

创建一个可排序集合的实例,例如ArrayList,添加值,然后对其进行排序:

List<ComparableObject> sortedCollection = new ArrayList<ComparableObject>(map.values());
Collections.sort(sortedCollection);

(B)

创建一个有序集合的实例,例如TreeSet,然后添加值:

Set<ComparableObject> sortedCollection = new TreeSet<ComparableObject>(map.values());

请注意,结果集合永远不会被修改,因此排序只需要进行一次。


这取决于输入数据的顺序-例如 如果您获取很多行并使用ORDER BY,那么这是一种情况-如果您有一组随机的Guid-另一种情况。
鲍里斯·特鲁霍夫

为什么不使用TreeMap呢?
托尔比约恩Ravn的安徒生

TreeMap在这里无济于事,因为排序需要根据值(ComparableObject)而不是键(Integer)进行。
gutch

3
另请注意,集合仅支持唯一条目。另一方面,HashMap的“值”集合可以包含重复项。从这个角度来看,TreeSet不是一个好的解决方案。
rompetroll

@gutch,您可能会在“ stackoverflow.com/questions/3759112/… ”中找到我的答案很有用。
理查德

Answers:


86

TreeSet具有方法的 log(n)时间复杂度保证add()/remove()/contains()。对ArrayListtakesn*log(n)操作进行排序,但add()/get()只进行1take操作。

因此,如果您主要是检索并且不经常排序,ArrayList则是更好的选择。如果您经常排序但不检索太多内容TreeSet将是更好的选择。


就我而言,我们只需要遍历结果集合,就永远不会对其进行修改。因此,根据您的答案ArrayList是这里的更好选择。
gutch

另外,数组排序可以并行完成,并具有更好的缓存性能。
kaiser

21

从理论上讲,排序应该更快。在整个过程中保持排序状态可能需要更多的CPU时间。

从CS的角度来看,这两个操作都是NlogN,但是1类应具有较低的常数。


4
+1理论与现实脱节的情况之一。:)以我的经验,最后排序往往快
几个

除非它们是O(N),否则将是整数数据。优先级队列还涉及O(log N)操作,以进行插入,删除和管理。
理查德

10

为什么不使用两全其美?如果不再使用它,请使用TreeSet排序并使用内容初始化ArrayList

List<ComparableObject> sortedCollection = 
    new ArrayList<ComparableObject>( 
          new TreeSet<ComparableObject>(map.values()));

编辑:

我已经创建了一个基准测试(您可以在pastebin.com/5pyPMJav上访问它)来测试这三种方法(ArrayList + Collections.sort,TreeSet和我两全其美的方法),并且我的方法总是成功的。测试文件创建了一个包含10000个元素的映射,这些元素的值有一个故意比较糟糕的比较器,然后,这三种策略中的每一种都有机会a)对数据进行排序并b)对其进行迭代。这是一些示例输出(您可以自己测试):

编辑:我添加了一个方面,可以记录对Thingy.compareTo(Thingy)的调用,并且我还添加了一个基于PriorityQueues的新策略,该策略比以前的任何一种解决方案都快得多(至少在排序方面)。

compareTo() calls:123490
Transformer ArrayListTransformer
    Creation: 255885873 ns (0.255885873 seconds) 
    Iteration: 2582591 ns (0.002582591 seconds) 
    Item count: 10000

compareTo() calls:121665
Transformer TreeSetTransformer
    Creation: 199893004 ns (0.199893004 seconds) 
    Iteration: 4848242 ns (0.004848242 seconds) 
    Item count: 10000

compareTo() calls:121665
Transformer BestOfBothWorldsTransformer
    Creation: 216952504 ns (0.216952504 seconds) 
    Iteration: 1604604 ns (0.001604604 seconds) 
    Item count: 10000

compareTo() calls:18819
Transformer PriorityQueueTransformer
    Creation: 35119198 ns (0.035119198 seconds) 
    Iteration: 2803639 ns (0.002803639 seconds) 
    Item count: 10000

奇怪的是,我的方法在迭代中表现最佳(我本以为在迭代中与ArrayList方法没有区别,基准测试中是否有错误?)

免责声明:我知道这可能是一个糟糕的基准,但是它可以帮助您理解要点,我当然没有操纵它来使自己的方法成功。

(该代码具有与equals / hashcode / compareTo构建器相同的apache commons / lang依赖性,但是应该很容易将其重构)


3
这难道不是两个世界中最糟糕的吗?我需要的只是自然顺序的集合,这就是new TreeSet<ComparableObject>(map.values())返回的结果。将其包裹起来ArrayList只会增加不必要的操作。
gutch

1
最终目标是排序的Collection......这TreeSet是。我看不到任何值将设置转换为列表。
Gunslinger47年

它没有包装,它正在初始化。并且arraylist更擅长检索,而树集更擅长排序
Sean Patrick Floyd

4
感谢您在编写基准测试方面付出的努力!但是我认为这是有缺陷的。看来JVM运行Transformer列表中较晚实例的速度比早期实例快:BestOfBothWorldsTransformer放在第一位,它突然运行得慢得多。因此,我重写了您的基准测试,以随机选择一个变压器并取平均值。在我的测试中,TreeSetTransformer持续跳动BestOfBothWorldsTransformer,也持续跳动ArrayListTransformer-根本不是我期望的!差别很小。参见pastebin.com/L0t5QDV9
gutch

1
我知道您的下一个问题是什么:PriorityQueueTransformer呢?难道不是比其他人快很多吗?是的,虽然顺序不正确,但太可惜了!看一下我上面的代码中每个转换器生成的列表,您会发现PriorityQueueTransformer实际上不是按顺序排列的!也许我使用PriorityQueue不正确?您是否有一个示例可以正确排序?
gutch

6

如果选择实现B,请务必阅读我在底部对TreeSet的评论。

如果您的应用程序仅偶尔进行排序,但要经过很多次迭代,那么我建议您最好使用简单的未排序列表。一次排序,然后受益于更快的迭代。在数组列表上,迭代特别快。

但是,如果您希望始终保证排序顺序,或者您可能经常添加/删除元素,则可以使用已排序的集合并进行迭代。

因此,在您的情况下,我会说A)是更好的选择。该列表仅排序一次,不会改变,因此可以从数组中受益。迭代应该非常快,尤其是如果您知道它的ArrayList并且可以直接使用ArrayList.get()而不是Iterator的话。

我还要添加一个定义,TreeSet是一个Set,这意味着对象是唯一的。TreeSet通过在Comparator / Comparable上使用compareTo来确定相等性。如果尝试添加两个其compareTo返回值为0的对象,则很容易发现自己丢失了数据。例如,将“ C”,“ A”,“ B”,“ A”添加到TreeSet中将返回“ A”,“ B” “, “C”


1
关于好点的TreeSet可能丢失数据如果的compareTo返回0。我已确定,在这种特定情况下的compareTo实施将不会返回0,这样既TreeSetArrayList将具有相同的行为。但是,在此之前,我已经被这个问题困扰了,非常感谢您的提醒!
gutch

PriorityQueue可能比TreeSet更好地对列表进行排序。
洛克

是的,在我的基准测试中(请参阅我的答案),PriorityQueue优于TreeSet 600%至700%。
肖恩·帕特里克·弗洛伊德

PriorityQueue确实确实执行得更快,但是当我尝试执行时,这些值实际上并未排序-显然为什么这么快!也许我误解了如何使用PriorityQueue ...一个实际起作用的示例将很有用。
gutch

PriorityQueue只是具有比较器/可比较测试的队列。当将add()项目添加到队列时,插入内容会将新项目与已经存在的项目进行比较,以确定要插入的位置。当您轮询()队列或对其进行迭代时,其内容已被排序。我希望插入是通过某种递归算法完成的,即将列表分成两部分并确定将其插入其中的一半,然后再分成两部分,依此类推,因此性能将达到O(log N),理论上与TreeSet / TreeMap,但实现可能会使其更快。
洛克

1

Collections.sort 使用具有O(nlog n)的mergeSort。

TreeSet具有底层的Red-Black树,基本操作具有O(logn)。因此,n个元素也具有O(nlog n)。

因此两者都是相同的大O算法。


6
尽管这听起来不错,但它可以弥补一些重要的成本。MergeSort的工作时间为O(n log n),但是Red-Black将需要O(n log n)进行插入,然后再次进行删除。big-O符号隐藏了算法中的重要差异。
理查德

0

插入SortedSet中的是O(log(n))(但是,当前n,而不是最后一个n)。插入列表为1。

插入中已经包括了SortedSet中的排序,因此它为0。列表中的排序为O(n * log(n))。

所以SortedSet的总复杂度为O(n * k),除最后一种情况外,所有情况下k <log(n)。相反,列表的总复杂度为O(n * log(n)+ n),因此O(n * log(n))。

因此,SortedSet在数学上具有最佳性能。但是最后,您有了一个Set而不是一个List(因为SortedList不存在),并且Set为您提供的功能少于List。因此,我认为,针对可用功能和性能的最佳解决方案是Sean Patrick Floyd提出的解决方案:

  • 使用SortedSet进行插入,
  • 将SortedSet作为创建要返回的List的参数。

0

好问题,好答案。只是想我会考虑一些要点:

  1. 例如,如果要排序的Collection是短暂的(例如,用作方法的参数),并且您需要在方法内对列表进行排序,则可以使用Collections.sort(collection)。或者,如果它是长期存在的对象,但是您很少需要对其进行排序。

理由:排序的集合是特定内容所必需的,您可能不会经常添加或删除。因此,对集合中的元素进行排序后,就不必再在意了。您基本上是:

排序->使用它->忘记

如果将新元素添加到已排序的集合中,则必须再次对集合进行排序,因为在插入新元素时不能保证顺序。

  1. 如果您的收藏进行排序的长寿命和/或如果它是一个类中的一个字段,你需要它的排序任何时候都那么你应该使用一个排序的数据结构,如TreeSet中。

理由:您一直在乎收款顺序。您希望始终对其进行排序。因此,如果您不断添加或删除元素,则可以保证对集合进行了排序。所以基本上:

插入/删除->使用它(只要能保证对集合进行了排序就可以)

没有特定的时刻需要对集合进行排序,相反,您希望一直对集合进行排序。

使用TreeSet的缺点是保留排序后的集合所需的资源。它使用一棵红黑树,并且获取,放置操作需要O(log n)时间成本。

而如果使用简单的集合(例如ArrayList),则get,add操作为O(1)恒定时间。

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.