我一直都喜欢树木,它们的优美O(n*log(n))
和整洁。但是,我认识的每个软件工程师都曾明确地问过我为什么要使用TreeSet
。从CS的背景出发,我认为您使用的所有内容都不重要,并且我也不在乎哈希函数和存储桶(对于Java
)。
在哪种情况下,我应该使用HashSet
over TreeSet
?
我一直都喜欢树木,它们的优美O(n*log(n))
和整洁。但是,我认识的每个软件工程师都曾明确地问过我为什么要使用TreeSet
。从CS的背景出发,我认为您使用的所有内容都不重要,并且我也不在乎哈希函数和存储桶(对于Java
)。
在哪种情况下,我应该使用HashSet
over TreeSet
?
Answers:
HashSet比TreeSet快得多(对于大多数操作(如添加,删除和包含),恒定时间与日志时间相对应),但没有提供像TreeSet这样的排序保证。
SortedSet
)first()
,last()
,headSet()
,和tailSet()
等HashSet
和之间TreeSet
。但是,它实现为散列表,散列表一直在其中运行,但是它提供了插入顺序的迭代,这与TreeSet保证的排序遍历不同。因此,用法的选择完全取决于您的需求,但是我认为,即使您需要有序的集合,您仍然应该更喜欢HashSet创建Set并将其转换为TreeSet。
SortedSet<String> s = new TreeSet<String>(hashSet);
尚未提到的a的一个优点TreeSet
是它具有更大的“局部性”,这简略地说:(1)如果两个条目按顺序相邻,则将TreeSet
它们在数据结构中并因此在内存中彼此靠近;(2)这种放置利用了局部性原理,即原理是相似的数据通常由具有相似频率的应用程序访问。
这与a相反HashSet
,后者将条目分散在整个内存中,而不管它们的键是什么。
当从硬盘驱动器读取的延迟成本是从缓存或RAM读取的延迟成本的数千倍时,并且实际上是通过本地访问数据时,这TreeSet
可能是一个更好的选择。
TreeSet
/ 的实现TreeMap
未优化局部性。虽然可以使用4阶的b树来表示红黑树,从而提高局部性和缓存性能,但这不是实现的工作原理。相反,每个节点都存储一个指向其自身键,其自身值,其父级以及左右子节点的指针,这在TreeMap.Entry的JDK 8源代码中显而易见。
HashSet
是O(1)来访问元素,因此它确实很重要。但是不可能保持对象在集合中的顺序。
TreeSet
如果维护订单(按值而不是插入顺序)对您很重要,则很有用。但是,正如您已经指出的那样,您在交易订单时需要花费更短的时间才能访问元素:O(log n)用于基本操作。
此实现提供了基本的操作保证的log(n)的时间成本(
add
,remove
和contains
)。
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
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);
根据@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 ║
╚══════════════╩═══════════════════════════════════════════════════════════════╝
使用最多的原因HashSet
是(平均)运算是O(1)而不是O(log n)。如果集合中包含标准项目,那么您将不会像“为哈希函数所困扰”那样为您完成操作。如果集合包含自定义类,则必须实现hashCode
使用HashSet
(尽管有效的Java显示了如何使用),但是如果使用,则TreeSet
必须使其Comparable
或提供Comparator
。如果类没有特定的顺序,则可能会出现问题。
有时我TreeSet
(或实际上TreeMap
)用于很小的集合/地图(<10个项),尽管我没有检查这样做是否有任何实际收益。对于大型设备,差异可能很大。
现在,如果需要排序,则TreeSet
比较合适,尽管即使更新频繁且对排序结果的需求很少,有时将内容复制到列表或数组并对其进行排序也可以更快。
HashSet的实现当然要快得多,因为不需要排序,因此开销较小。http://java.sun.com/docs/books/tutorial/collections/implementations/set.html提供了对Java中各种Set实现的很好分析。。
那里的讨论还指出了“树与哈希”问题的有趣的“中间立场”方法。Java提供了一个LinkedHashSet,它是一个运行着“面向插入”的链表的HashSet,也就是说,链表中的最后一个元素也是最新插入到Hash中的元素。这使您可以避免无序哈希的不规则性,而不会增加TreeSet的成本。
该TreeSet中是两个排序集合(另一个是TreeMap中)之一。它使用红黑树结构(但您知道的),并保证元素按照自然顺序升序排列。(可选)您可以使用构造函数构造一个TreeSet,该构造函数使您可以使用Comparable或Comparator为集合指定自己的规则(而不是依赖元素类定义的顺序)
和A LinkedHashSet是维护所有元素双链表的HashSet的有序版本。当您关心迭代顺序时,请使用此类而不是HashSet。当您遍历HashSet时,顺序是不可预测的,而LinkedHashSet可让您按插入元素的顺序来遍历元素。
基于技术考虑,尤其是在性能方面,已经给出了很多答案。在我看来,在TreeSet
和之间进行选择很HashSet
重要。
但是我宁愿说,选择应该首先从概念上考虑。
如果对于您需要操纵的对象而言,自然排序没有意义,则不要使用TreeSet
。
由于已实现,因此是一个有序集SortedSet
。因此,这意味着您需要重写函数compareTo
,该函数应与返回函数的内容一致equals
。例如,如果您有一个名为Student的类的对象集,那么我认为TreeSet
有意义的一致,因为学生之间没有自然的顺序。您可以按它们的平均等级对其进行排序,好吧,但这不是“自然排序”。功能compareTo
不仅在两个对象代表同一学生时,而且在两个不同学生的成绩相同时,都将返回0。对于第二种情况,equals
将返回false(除非您决定当两个不同的学生具有相同的年级时使后者返回true,这将使equals
函数具有误导性的含义,而不是说错的含义。)
请注意equals
和之间的这种一致性compareTo
是可选的,但强烈建议使用。否则,接口的约定将Set
被破坏,使您的代码误导他人,从而也可能导致意外行为。
此链接可能是有关此问题的良好信息来源。
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));
}
}