[英]Hashset vs Treeset
我一直很喜歡樹木,那漂亮的O(n*log(n))
和它們的整潔。 然而,我認識的每一位軟件工程師都尖銳地問我為什么要使用TreeSet
。 從 CS 背景來看,我認為你使用什么並不重要,而且我也不關心散列函數和存儲桶(在Java
的情況下)。
在哪些情況下我應該在TreeSet
使用HashSet
?
HashSet 比 TreeSet 快得多(對於大多數操作,如添加、刪除和包含,常量時間與日志時間)但不提供像 TreeSet 那樣的排序保證。
SortedSet
)first()
、 last()
、 headSet()
和tailSet()
等HashSet
和TreeSet
之間。 實現為一個帶有鏈表的哈希表,但是,它提供了插入順序迭代,這與 TreeSet 保證的排序遍歷不同。因此,用法的選擇完全取決於您的需要,但我覺得即使您需要一個有序集合,您仍然應該更喜歡 HashSet 來創建 Set 然后將其轉換為 TreeSet。
SortedSet<String> s = new TreeSet<String>(hashSet);
TreeSet
還沒有提到的一個優點是它具有更大的“局部性”,這是說 (1) 如果兩個條目在順序中靠近,則TreeSet
將它們放置在數據結構中彼此靠近,因此在內存中; (2) 這種放置利用了局部性原則,即相似的數據經常被具有相似頻率的應用程序訪問。
這與HashSet
形成對比,后者將條目分布在整個內存中,無論它們的鍵是什么。
當從硬盤讀取的延遲成本是從緩存或 RAM 讀取的成本的數千倍時,並且當數據確實是局部訪問時, TreeSet
可能是一個更好的選擇。
根據@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 ║
╚══════════════╩═══════════════════════════════════════════════════════════════╝
1.HashSet 允許空對象。
2.TreeSet 不允許空對象。 如果您嘗試添加空值,它將拋出 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
大多數使用HashSet
的原因是操作(平均)是 O(1) 而不是 O(log n)。 如果該集合包含標准項目,您將不會像已經為您完成的那樣“弄亂哈希函數”。 如果集合包含自定義類,則必須實現hashCode
以使用HashSet
(盡管 Effective Java 顯示了如何),但如果使用TreeSet
,則必須使其成為Comparable
或提供Comparator
。 如果類沒有特定順序,這可能是一個問題。
我有時將TreeSet
(或實際上TreeMap
)用於非常小的集合/地圖(< 10 個項目),盡管我沒有檢查這樣做是否有任何實際收益。 對於大型集合,差異可能相當大。
現在,如果您需要排序,那么TreeSet
是合適的,盡管即使這樣更新頻繁並且對排序結果的需求很少,有時將內容復制到列表或數組並對其進行排序可能會更快。
如果您沒有插入足夠多的元素來導致頻繁的重新散列(或沖突,如果您的 HashSet 無法調整大小),那么 HashSet 肯定會給您帶來恆定時間訪問的好處。 但是在有大量增長或收縮的集合上,根據實現,您實際上可能會使用 Treesets 獲得更好的性能。
如果沒有記錯的話,使用功能性紅黑樹的攤銷時間可以接近 O(1)。 Okasaki 的書會比我能得到更好的解釋。 (或見他的出版清單)
HashSet 的實現當然要快得多——因為沒有排序,所以開銷更少。 http://java.sun.com/docs/books/tutorial/collections/implementations/set.html提供了對 Java 中各種 Set 實現的良好分析。
那里的討論還指出了一種有趣的“中間立場”方法來解決樹與哈希問題。 Java提供了一個LinkedHashSet,它是一個“面向插入”的鏈表貫穿其中的HashSet,即鏈表中的最后一個元素也是最近插入到Hash中的元素。 這允許您避免無序散列的不規則性,而不會增加 TreeSet 的成本。
當你可以吃橘子時,為什么要吃蘋果?
說真的,伙計們 - 如果您的集合很大,讀取和寫入無數次,並且您為 CPU 周期付費,那么僅當您需要它以提高性能時,集合的選擇才有意義。 然而,在大多數情況下,這並不重要——這里和那里的幾毫秒在人類看來是被忽視的。 如果真的那么重要,為什么不用匯編程序或 C 編寫代碼? [提示另一個討論]。 所以關鍵是,如果你喜歡使用你選擇的任何集合,並且它解決了你的問題[即使它不是任務的最佳集合類型] 把你自己擊倒。 該軟件具有可塑性。 必要時優化您的代碼。 鮑勃叔叔說過早優化是萬惡之源。 鮑勃叔叔這么說
TreeSet是兩個排序集合之一(另一個是 TreeMap)。 它使用紅黑樹結構(但您知道這一點),並保證元素按照自然順序按升序排列。 或者,您可以使用構造函數構造一個 TreeSet,該構造函數允許您使用 Comparable 或 Comparator 為集合提供自己的順序規則(而不是依賴於元素的類定義的順序)
LinkedHashSet是 HashSet 的有序版本,它維護一個跨所有元素的雙向鏈表。 當您關心迭代順序時,請使用此類而不是 HashSet。 當您遍歷 HashSet 時,順序是不可預測的,而 LinkedHashSet 允許您按照元素插入的順序遍歷元素
即使在 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));
嘗試猜測輸出,然后將鼠標懸停在片段下方以查看實際輸出是什么。 准備好? 干得好:
錯誤的
真的
沒錯,對於與 equals 不一致的比較器,它們不存在等價關系。 這樣做的原因是TreeSet
使用比較器來確定等價性,而HashSet
使用equals
。 他們在內部使用HashMap
和TreeMap
所以你應該期待提到的Map
的這種行為。
消息編輯(完全重寫) 當順序無關緊要時。 兩者都應該給出 Log(n) - 看看其中一個是否比另一個快 5% 以上將是有用的。 HashSet 可以在循環中進行 O(1) 測試,應該揭示它是否是。
基於技術考慮,特別是性能方面的考慮,已經給出了很多答案。 在我看來,在TreeSet
和HashSet
之間進行選擇很重要。
considerations first.但我更願意說選擇應該首先由考慮驅動。
如果對於您需要操作的對象,自然排序沒有意義,則不要使用TreeSet
。
它是一個有序集合,因為它實現了SortedSet
。 所以這意味着你需要覆蓋函數compareTo
,它應該與返回函數equals
一致。 例如,如果您有一組名為 Student 的類的對象,那么我認為TreeSet
沒有意義,因為學生之間沒有自然排序。 你可以按他們的平均成績來排序,好吧,但這不是“自然排序”。 函數compareTo
不僅在兩個對象代表同一學生時返回 0,而且當兩個不同學生的成績相同時也會返回 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));
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.