哈希集与树集

Posted

技术标签:

【中文标题】哈希集与树集【英文标题】:Hashset vs Treeset 【发布时间】:2010-11-30 14:25:56 【问题描述】:

我一直很喜欢树木,很漂亮O(n*log(n)) 和它们的整洁。然而,我认识的每一位软件工程师都尖锐地问我为什么要使用TreeSet。从 CS 的背景来看,我认为你使用什么并不重要,我也不想乱用哈希函数和存储桶(在 Java 的情况下)。

在哪些情况下我应该使用HashSet 而不是TreeSet

【问题讨论】:

【参考方案1】:

HashSet 比 TreeSet 快得多(对于添加、删除和包含等大多数操作而言,HashSet 是常数时间与日志时间),但不提供像 TreeSet 那样的排序保证。

HashSet

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

TreeSet

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

要点:

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

所以使用方式的选择完全取决于你的需求,但我觉得即使你需要一个有序的集合,你仍然应该更喜欢 HashSet 来创建 Set 然后将其转换为 TreeSet。

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

【讨论】:

只有我发现“HashSet 比 TreeSet 快得多(常量时间与日志时间......)”这句话显然是错误的?首先,这是关于时间复杂度的,而不是绝对时间,而且 O(1) 在很多情况下可能比 O(f(N)) 慢。其次,O(logN)“几乎”O(1)。如果在许多常见情况下 TreeSet 优于 HashSet,我不会感到惊讶。 我只想支持 Ivella 的评论。时间复杂度 NOT 与运行时间相同,而且 O(1) 并不总是比 O(2^n) 好。一个反常的例子说明了这一点:考虑一个使用 1 万亿条机器指令执行 (O(1)) 的哈希算法的哈希集与 10 个元素的冒泡排序 (O(N^2) avg/worst) 的任何常见实现.冒泡排序每次都会获胜。重点是算法课程教每个人使用时间复杂度来考虑近似值,但在现实世界中,常数因子 MATTER 经常出现。 也许只有我一个人,但首先将所有内容添加到哈希集,然后将其转换为树集的建议不是很糟糕吗? 1) 仅当您提前知道数据集的大小时,才可以快速插入哈希集中,否则您可能需要支付 O(n) 重新散列,可能多次。和 2) 在转换集合时,无论如何您都要为 TreeSet 插入付费。 (复仇,因为通过哈希集的迭代效率不是很高) 这个建议是基于这样一个事实,对于一个集合,你必须在添加之前检查一个项目是否是重复的;因此,如果您在树集上使用哈希集,您将节省消除重复项的时间。然而,考虑到为非重复创建第二组所付出的代价,重复的百分比应该非常高,以克服这个代价并节省时间。当然,这适用于中型和大型集,因为对于小型集,树集可能比哈希集更快。 @PeterOehlert:请为此提供一个基准。我理解你的意思,但是两组之间的差异对于小集合大小几乎没有关系。一旦集合增长到某个点,实现很重要,log(n) 就会成为一个问题。一般来说,散列函数(甚至是复杂函数)的数量级比几个缓存未命中(对于几乎每个访问级别都有巨大的树)来查找/访问/添加/修改叶子要快。至少这是我在 Java 中使用这两组的经验。【参考方案2】:

TreeSet 尚未提及的一个优点是它具有更大的“局部性”,这是表示 (1) 如果两个条目在顺序中靠近,TreeSet 将它们彼此靠近放置在数据结构,因此在内存中; (2) 这种布局利用了局部性原则,即应用程序经常以相似的频率访问相似的数据。

这与HashSet 形成对比,HashSet 将条目分布在整个内存中,无论它们的键是什么。

当从硬盘读取的延迟成本是从缓存或 RAM 读取的成本的数千倍时,并且当数据真正通过本地访问时,TreeSet 可能是更好的选择。

【讨论】:

你能证明如果两个条目按顺序靠近,TreeSet 将它们放在数据结构中彼此靠近,因此在内存中 与 Java 完全无关。集合的元素无论如何都是对象并指向其他地方,因此您不会节省太多任何东西。 除了其他 cmets 对 Java 中普遍缺乏局部性提出的看法外,OpenJDK 对 TreeSet/TreeMap 的实现并未进行局部优化。虽然可以使用 4 阶 b-tree 来表示红黑树,从而提高局部性和缓存性能,但这不是实现的工作方式。相反,每个节点都存储一个指向它自己的键、它自己的值、它的父节点以及它的左右子节点的指针,这在JDK 8 source code for TreeMap.Entry 中很明显。【参考方案3】:

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

TreeSet 如果维护订单(就价值而非插入顺序而言)对您很重要,则很有用。但是,正如您所指出的,您正在交易订单以换取访问元素的更慢时间:基本操作为 O(log n)。

来自javadocs for TreeSet

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

【讨论】:

【参考方案4】:

根据@shevchyk 在地图上可爱的visual answer,这是我的看法:

╔══════════════╦═════════════════════╦═══════════════════╦═════════════════════╗
║   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               ║
╚══════════════╩═══════════════════════════════════════════════════════════════╝

【讨论】:

【参考方案5】:

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

【讨论】:

ts.add(null) 如果将 null 添加为 TreeSet 中的第一个对象,则它在 TreeSet 的情况下可以正常工作。之后添加的任何对象都会在 Comparator 的 compareTo 方法中给出 NullPointerException。 你真的不应该在你的集合中添加null TreeSet&lt;String&gt; badassTreeSet = new TreeSet&lt;String&gt;(new Comparator&lt;String&gt;() 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); @ShoaibChikate 您的说法在我的 Java 版本(Oracle Corporation 11.0.4+10-LTS)中不准确。插入的第一个元素始终与自身进行比较,因此如果第一个元素是 null,则抛出 NullPointerException 严格来说这不是真的。如果TreeSet 是使用允许空值的比较器创建的,则可以添加空值。每TreeSet.add(E e):“抛出:NullPointerException - 如果指定元素为空且此集合使用自然排序,或者其比较器不允许空元素”。这成功地将null 添加到TreeSetnew TreeSet&lt;&gt;(Comparator.nullsLast(Comparator.naturalOrder())).add(null);【参考方案6】:

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

我有时将TreeSet(或实际上TreeMap)用于非常小的集合/地图(

现在,如果您需要排序,那么TreeSet 是合适的,尽管即使更新频繁并且对排序结果的需求很少,有时将内容复制到列表或数组并对其进行排序会更快.

【讨论】:

这些大元素上的任何数据点,例如 10K 或更多【参考方案7】:

如果您没有插入足够多的元素而导致频繁的重新散列(或冲突,如果您的 HashSet 无法调整大小),那么 HashSet 肯定会为您提供恒定时间访问的好处。但在具有大量增长或收缩的集合上,您实际上可能会使用 Treesets 获得更好的性能,具体取决于实现。

如果我没记错的话,使用功能红黑树的摊销时间可以接近 O(1)。冈崎的书会有比我能理解的更好的解释。 (或见his publication list)

【讨论】:

【参考方案8】:

HashSet 的实现当然要快得多——因为没有排序,所以开销更少。 http://java.sun.com/docs/books/tutorial/collections/implementations/set.html 提供了对 Java 中各种 Set 实现的良好分析。

那里的讨论还指出了一种有趣的“中间立场”方法来解决 Tree vs Hash 问题。 Java提供了一个LinkedHashSet,它是一个HashSet,其中运行着一个“面向插入”的链表,即链表中的最后一个元素也是最近插入到Hash中的元素。这使您可以避免无序散列的不规则性,而不会增加 TreeSet 的成本。

【讨论】:

【参考方案9】:

TreeSet 是两个排序集合之一(另一个是 树形图)。它使用红黑树结构(但你知道),并保证 根据自然顺序,元素将按升序排列。可选地, 您可以使用构造函数构造一个 TreeSet,该构造函数可让您为集合提供您的 自己的顺序应该是什么规则(而不是依赖于定义的顺序 通过元素的类)通过使用 Comparable 或 Comparator

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

【讨论】:

【参考方案10】:

既然可以吃橙子,为什么还要吃苹果?

说真的,伙计们 - 如果您的集合很大,读取和写入的次数达到了无数次,并且您要为 CPU 周期付费,那么只有当您需要更好地执行该集合时,该集合的选择才是相关的。然而,在大多数情况下,这并不重要——从人类的角度来看,这里和那里的几毫秒都不会被注意到。如果它真的那么重要,你为什么不用汇编程序或 C 编写代码呢? [提示另一个讨论]。所以关键是,如果您对使用您选择的任何集合感到满意,并且它解决了您的问题(即使它不是专门针对该任务的最佳集合类型),那么您就会被淘汰出局。该软件具有延展性。在必要时优化您的代码。 Bob 大叔说,过早优化是万恶之源。 Uncle Bob says so

【讨论】:

即使您通过Set&lt;T&gt; 引用使用您的集合,您也必须立即选择一个具体的类来实例化它,并提供所需的方法(equals、compare、hashcode)。不是没有优化,只是尝试做出适当的选择,这样您以后就不必更改它。【参考方案11】:

即使在 11 年后,也没有人想到提及一个非常重要的差异。

你认为如果HashSet 等于TreeSet 那么相反的情况也是如此吗?看看这段代码:

TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
HashSet<String> hashSet = new HashSet<>();
treeSet.add("a");
hashSet.add("A");
System.out.println(hashSet.equals(treeSet));
System.out.println(treeSet.equals(hashSet));

尝试猜测输出,然后将鼠标悬停在 sn-p 下方以查看实际输出是什么。准备好?给你:

假 真的

没错,对于与 equals 不一致的比较器,它们不具有等价关系。原因是TreeSet 使用比较器来确定等价性,而HashSet 使用equals。在内部,他们使用HashMapTreeMap,因此您也应该预料到上述Maps 也会出现这种行为。

Originally answered

【讨论】:

【参考方案12】:

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

【讨论】:

【参考方案13】:

基于技术考虑,尤其是在性能方面,已经给出了很多答案。 在我看来,TreeSetHashSet 之间的选择很重要。 但我宁愿说,选择应该首先由概念考虑。 如果对于您需要操作的对象,自然排序没有意义,那么不要使用TreeSet。 它是一个排序集,因为它实现了SortedSet。所以这意味着你需要重写函数compareTo,这应该与返回函数equals的内容一致。例如,如果您有一组名为 Student 的类的对象,那么我认为 TreeSet 没有意义,因为学生之间没有自然排序。你可以按他们的平均成绩排序,好吧,但这不是“自然排序”。函数compareTo 不仅会在两个对象代表同一个学生时返回 0,而且当两个不同学生的年级相同时也会返回 0。对于第二种情况,equals 将返回 false(除非您决定让后者在两个不同学生的成绩相同时返回 true,这会使 equals 函数具有误导性的含义,而不是说错误的含义。) 请注意equalscompareTo 之间的一致性是可选的,但强烈推荐。否则接口Set的约定被破坏,使你的代码误导其他人,从而也可能导致意外行为。

这个 link 可能是关于这个问题的一个很好的信息来源。

【讨论】:

【参考方案14】:
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));
    

【讨论】:

博文中说一般情况下,将元素添加到HashSet中,然后将集合转换为TreeSet进行无重复排序遍历会更快。 Set s = new TreeSet(hashSet);我想知道为什么不直接 Set s = new TreeSet() 如果我们知道它将用于排序迭代,所以我做了这个比较,结果显示哪个更快。 "在哪些情况下我想在 TreeSet 上使用 HashSet?" 我的观点是,如果您需要排序,单独使用 TreeSet 比将所有内容都放入 HashSet 然后基于该 HashSet 创建一个 TreeSet 更好。我从原始帖子中根本看不到 HashSet + TreeSet 的值。 @gli00001:你没抓住重点。如果您不总是需要对您的元素集进行排序,而是要经常对其进行操作,那么使用哈希集来从更快的操作中受益是值得的大多数时候。对于需要按顺序处理元素的偶尔时间,只需用树集包装即可。这取决于您的用例,但这并不是一个不常见的用例(并且可能假设一个集合不包含太多元素且具有复杂的排序规则)。

以上是关于哈希集与树集的主要内容,如果未能解决你的问题,请参考以下文章

Python算法教程第二章知识点:计时模块字典与散哈希表图与树的实现成员查询插入对象

redis学习

实用java知识点总结

哈希算法和哈希表的区别?

哈希表、哈希算法、一致性哈希表

哈希表哈希桶的实现