哈希集与树集


495

我一直都喜欢树木,它们的优美O(n*log(n))和整洁。但是,我认识的每个软件工程师都曾明确地问过我为什么要使用TreeSet。从CS的背景出发,我认为您使用的所有内容都不重要,并且我也不在乎哈希函数和存储桶(对于Java)。

在哪种情况下,我应该使用HashSetover TreeSet

Answers:


859

HashSet比TreeSet快得多(对于大多数操作(如添加,删除和包含),恒定时间与日志时间相对应),但没有提供像TreeSet这样的排序保证。

哈希集

  • 该类为基本操作(添加,删除,包含和调整大小)提供恒定的时间性能。
  • 它不能保证元素的顺序随时间保持不变
  • 迭代性能取决于HashSet 的初始容量负载因子
    • 接受默认的负载系数是非常安全的,但是您可能希望指定一个初始容量,该容量大约是预期的增长容量的两倍。

树集

  • 保证基本操作(添加,删除和包含)的log(n)时间成本
  • 保证set的元素将被排序(升序,自然或您通过其构造函数指定的元素)(实现 SortedSet
  • 没有为迭代性能提供任何调整参数
  • 提供了一些方便的方法来处理的有序集合一样first()last()headSet(),和tailSet()

重要事项:

  • 两者都保证无重复的元素集合
  • 通常,向HashSet中添加元素,然后将集合转换为TreeSet进行无重复排序遍历的速度更快。
  • 这些实现均未同步。也就是说,如果多个线程同时访问一个集,并且至少有一个线程修改了该集,则必须在外部对其进行同步。
  • LinkedHashSet在某种意义上介于HashSet和之间TreeSet。但是,实现为散列表,散列表一直在其中运行,但是它提供了插入顺序的迭代,这与TreeSet保证的排序遍历不同

因此,用法的选择完全取决于您的需求,但是我认为,即使您需要有序的集合,您仍然应该更喜欢HashSet创建Set并将其转换为TreeSet。

  • 例如 SortedSet<String> s = new TreeSet<String>(hashSet);

38
只是我发现肯定的说法“ HashSet比TreeSet快得多(恒定时间与日志时间...)”显然是错误的吗?首先,这是关于时间复杂性,而不是绝对时间,并且O(1)在许多情况下可能比O(f(N))慢。其次,O(logN)是“几乎” O(1)。如果在许多常见情况下TreeSet的性能优于HashSet,我不会感到惊讶。
lvella 2012年

22
我只想说一下伊薇拉的评论。时间复杂度是一样的东西运行时间,以及O(1)并不总是为O更好的(2 ^ N)。一个不正当的例子说明了这一点:考虑使用1万亿条机器指令执行(O(1))的哈希算法与10个元素的冒泡排序的任何常见实现(O(N ^ 2)平均/最差)的哈希集。气泡排序每次都会赢。问题的关键是算法类教大家考虑使用时间复杂度近似,但在现实世界中的常数因子频繁。
彼得·欧勒

17
也许只是我一个人,但不是建议先将所有内容添加到哈希集中,然后再将其隐藏到树集上却是一个可怕的建议吗?1)仅在事先知道数据集的大小的情况下,才快速插入哈希集中,否则,您需要进行一次O(n)的重新哈希处理,可能是多次。和2)无论如何,在转换树集时都要为插入树集付费。(复仇,因为通过哈希集进行迭代效率不高)
TinkerTank 2012年

5
该建议基于以下事实:对于一组商品,您必须在添加商品之前检查其是否为重复商品;因此,如果您在树集上使用哈希集,则可以节省消除重复项的时间。但是,考虑到为非重复项创建第二组所要付出的代价,重复项的百分比确实应该很大,以克服此价格并节省时间。当然,这适用于中型和大型集,因为对于小型集,树集可能比哈希集更快。
SylvainL 2012

5
@PeterOehlert:请为此提供一个基准。我理解您的观点,但是对于较小的集合大小,两组之间的区别几乎没有关系。一旦集合增长到一定程度,实现就很重要,log(n)就会成为问题。通常,散列函数(甚至是复杂的散列函数)的数量级要比几个高速缓存未命中(在几乎每个访问级别的大型树上都具有)要查找/访问/添加/修改叶的速度要快。至少那是我在Java中使用这两套代码的经验。
Bouncner 2013年

38

尚未提到的a的一个优点TreeSet是它具有更大的“局部性”,这简略地说:(1)如果两个条目按顺序相邻,则将TreeSet它们在数据结构中并因此在内存中彼此靠近;(2)这种放置利用了局部性原理,即原理是相似的数据通常由具有相似频率的应用程序访问。

这与a相反HashSet,后者将条目分散在整个内存中,而不管它们的键是什么。

当从硬盘驱动器读取的延迟成本是从缓存或RAM读取的延迟成本的数千倍时,并且实际上是通过本地访问数据时,这TreeSet可能是一个更好的选择。


3
您能否证明如果两个条目按顺序位于附近,则TreeSet会将它们在数据结构中并因此在内存中彼此靠近放置
大卫·索罗科

6
与Java完全无关。该集合的元素无论如何都是对象,并且指向其他地方,因此您不会节省任何东西。
安德鲁·加拉施 Andrew Gallasch)2015年

除了关于Java普遍缺乏局部性的其他评论外,OpenJDK的TreeSet/ 的实现TreeMap未优化局部性。虽然可以使用4阶的b树来表示红黑树,从而提高局部性和缓存性能,但这不是实现的工作原理。相反,每个节点都存储一个指向其自身键,其自身值,其父级以及左右子节点的指针,这在TreeMap.EntryJDK 8源代码中显而易见
kbolino

25

HashSet是O(1)来访问元素,因此它确实很重要。但是不可能保持对象在集合中的顺序。

TreeSet如果维护订单(按值而不是插入顺序)对您很重要,则很有用。但是,正如您已经指出的那样,您在交易订单时需要花费更短的时间才能访问元素:O(log n)用于基本操作。

javadocs中获取TreeSet

此实现提供了基本的操作保证的log(n)的时间成本(addremovecontains)。


22

1.HashSet允许空对象。

2.TreeSet将不允许空对象。如果尝试添加null值,则将抛出NullPointerException。

3.HashSet比TreeSet快得多。

例如

 TreeSet<String> ts = new TreeSet<String>();
 ts.add(null); // throws NullPointerException

 HashSet<String> hs = new HashSet<String>();
 hs.add(null); // runs fine

3
ts.add(null)如果将TreeSet中的第一个Object添加为null,则在TreeSet的情况下可以正常工作。之后添加的任何对象都将在Comparator的compareTo方法中提供NullPointerException。
Shoaib Chikate,2015年

2
您真的不应该null以任何一种方式添加到您的集合中。
蓬松的

TreeSet<String> badassTreeSet = new TreeSet<String>(new Comparator<String>() { public int compare(String string1, String string2) { if (string1 == null) { return (string2 == null) ? 0 : -1; } else if (string2 == null) { return 1; } else { return string1.compareTo(string2); } } }); badassTreeSet.add("tree"); badassTreeSet.add("asdf"); badassTreeSet.add(null); badassTreeSet.add(null); badassTreeSet.add("set"); badassTreeSet.add("tree"); System.out.println(badassTreeSet);
大卫·霍瓦特

21

根据@shevchyk在地图上提供的可爱视觉答案,这是我的看法:

╔══════════════╦═════════════════════╦═══════════════════╦═════════════════════╗
   Property          HashSet             TreeSet           LinkedHashSet   
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                no guarantee order  sorted according                       
   Order       will remain constant to the natural        insertion-order  
                    over time          ordering                            
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
 Add/remove           O(1)              O(log(n))             O(1)         
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                                      NavigableSet                         
  Interfaces           Set                Set                  Set         
                                       SortedSet                           
╠══════════════╬═════════════════════╬═══════════════════╬═════════════════════╣
                                       not allowed                         
  Null values        allowed        1st element only        allowed        
                                        in Java 7                          
╠══════════════╬═════════════════════╩═══════════════════╩═════════════════════╣
                 Fail-fast behavior of an iterator cannot be guaranteed      
   Fail-fast   impossible to make any hard guarantees in the presence of     
   behavior              unsynchronized concurrent modification              
╠══════════════╬═══════════════════════════════════════════════════════════════╣
      Is                                                                     
 synchronized               implementation is not synchronized               
╚══════════════╩═══════════════════════════════════════════════════════════════╝

13

使用最多的原因HashSet是(平均)运算是O(1)而不是O(log n)。如果集合中包含标准项目,那么您将不会像“为哈希函数所困扰”那样为您完成操作。如果集合包含自定义类,则必须实现hashCode使用HashSet(尽管有效的Java显示了如何使用),但是如果使用,则TreeSet必须使其Comparable或提供Comparator。如果类没有特定的顺序,则可能会出现问题。

有时我TreeSet(或实际上TreeMap)用于很小的集合/地图(<10个项),尽管我没有检查这样做是否有任何实际收益。对于大型设备,差异可能很大。

现在,如果需要排序,则TreeSet比较合适,尽管即使更新频繁且对排序结果的需求很少,有时将内容复制到列表或数组并对其进行排序也可以更快。


这些大型元素(例如10K或更多)的任何数据点
kuhajeyan

11

如果您没有插入足够多的元素来导致频繁的重新哈希处理(或冲突,如果您的HashSet无法调整大小),则HashSet当然可以为您带来持续访问的好处。但是,在具有大量增长或收缩的集合上,根据实现的不同,使用树集实际上可能会获得更好的性能。

如果内存为我服务,则使用功能正常的红黑树的摊销时间可以接近O(1)。冈崎的书比我能提出的更好的解释。(或查看他的出版物清单


7

HashSet的实现当然要快得多,因为不需要排序,因此开销较小。http://java.sun.com/docs/books/tutorial/collections/implementations/set.html提供了对Java中各种Set实现的很好分析。

那里的讨论还指出了“树与哈希”问题的有趣的“中间立场”方法。Java提供了一个LinkedHashSet,它是一个运行着“面向插入”的链表的HashSet,也就是说,链表中的最后一个元素也是最新插入到Hash中的元素。这使您可以避免无序哈希的不规则性,而不会增加TreeSet的成本。


4

TreeSet中是两个排序集合(另一个是TreeMap中)之一。它使用红黑树结构(但您知道的),并保证元素按照自然顺序升序排列。(可选)您可以使用构造函数构造一个TreeSet,该构造函数使您可以使用Comparable或Comparator为集合指定自己的规则(而不是依赖元素类定义的顺序)

和A LinkedHashSet是维护所有元素双链表的HashSet的有序版本。当您关心迭代顺序时,请使用此类而不是HashSet。当您遍历HashSet时,顺序是不可预测的,而LinkedHashSet可让您按插入元素的顺序来遍历元素。


3

基于技术考虑,尤其是在性能方面,已经给出了很多答案。在我看来,在TreeSet和之间进行选择很HashSet重要。

但是我宁愿说,选择应该首先从概念上考虑。

如果对于您需要操纵的对象而言,自然排序没有意义,则不要使用TreeSet
由于已实现,因此是一个有序集SortedSet。因此,这意味着您需要重写函数compareTo,该函数应与返回函数的内容一致equals。例如,如果您有一个名为Student的类的对象集,那么我认为TreeSet有意义的一致,因为学生之间没有自然的顺序。您可以按它们的平均等级对其进行排序,好吧,但这不是“自然排序”。功能compareTo不仅在两个对象代表同一学生时,而且在两个不同学生的成绩相同时,都将返回0。对于第二种情况,equals将返回false(除非您决定当两个不同的学生具有相同的年级时使​​后者返回true,这将使equals函数具有误导性的含义,而不是说错的含义。)
请注意equals和之间的这种一致性compareTo是可选的,但强烈建议使用。否则,接口的约定将Set被破坏,使您的代码误导他人,从而也可能导致意外行为。

链接可能是有关此问题的良好信息来源。


3

当您可以吃橙子时为什么要吃苹果?

认真的对待家伙和女孩-如果您的集合很大,需要读写数以千计的次,并且您要为CPU周期付费,那么只有在需要更好的性能时才需要选择集合。但是,在大多数情况下,这并不重要-用人为术语来回移动这几毫秒就不会引起注意。如果真的很重要,为什么不用汇编器或C编写代码呢?[提示另一个讨论]。因此,关键是如果您愿意使用所选的任何集合,并且可以解决您的问题(即使不是专门针对任务的最佳集合类型),也可以自行解决。该软件具有延展性。必要时优化代码。鲍伯叔叔说过早的优化是万恶之源。鲍伯叔叔这么说


1

消息编辑(完全重写)当顺序无关紧要时,那就是。两者都应给出Log(n)-看看其中一个是否比另一个快5%以上将很有用。HashSet可以在循环中提供O(1)测试,以揭示是否存在。


-3
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class HashTreeSetCompare {

    //It is generally faster to add elements to the HashSet and then
    //convert the collection to a TreeSet for a duplicate-free sorted
    //Traversal.

    //really? 
    O(Hash + tree set) > O(tree set) ??
    Really???? Why?



    public static void main(String args[]) {

        int size = 80000;
        useHashThenTreeSet(size);
        useTreeSetOnly(size);

    }

    private static void useTreeSetOnly(int size) {

        System.out.println("useTreeSetOnly: ");
        long start = System.currentTimeMillis();
        Set<String> sortedSet = new TreeSet<String>();

        for (int i = 0; i < size; i++) {
            sortedSet.add(i + "");
        }

        //System.out.println(sortedSet);
        long end = System.currentTimeMillis();

        System.out.println("useTreeSetOnly: " + (end - start));
    }

    private static void useHashThenTreeSet(int size) {

        System.out.println("useHashThenTreeSet: ");
        long start = System.currentTimeMillis();
        Set<String> set = new HashSet<String>();

        for (int i = 0; i < size; i++) {
            set.add(i + "");
        }

        Set<String> sortedSet = new TreeSet<String>(set);
        //System.out.println(sortedSet);
        long end = System.currentTimeMillis();

        System.out.println("useHashThenTreeSet: " + (end - start));
    }
}

1
该帖子说,通常将元素添加到HashSet并将集合转换为TreeSet更快,以便进行无重复的排序遍历。Set <String> s =新的TreeSet <String>(hashSet); 我想知道为什么不直接将Set <String> s = new TreeSet <String>()直接用于排序迭代,所以我进行了比较,结果表明更快。
gli00001 2012年

“在哪种情况下,我想在TreeSet上使用HashSet?”
奥斯汀·亨利

1
我的观点是,如果需要订购,单独使用TreeSet优于将所有内容放入HashSet中,然后根据该HashSet创建一个TreeSet更好。我从原始帖子中根本看不到HashSet + TreeSet的值。
gli00001

@ gli00001:您错过了重点。如果您不总是需要对元素集进行排序,而是要经常对其进行排序,那么对于大多数时间来说,使用哈希集来受益于更快的操作将是值得的。在偶尔需要按顺序处理元素的情况下,只需用树集包装即可。这取决于您的用例,但这不是一个罕见的用例(它可能假定一个不包含太多元素且具有复杂排序规则的集合)。
haylem 2012年
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.