簡體   English   中英

哈希集與樹集

[英]Hashset vs Treeset

我一直很喜歡樹木,那漂亮的O(n*log(n))和它們的整潔。 然而,我認識的每一位軟件工程師都尖銳地問我為什么要使用TreeSet 從 CS 背景來看,我認為你使用什么並不重要,而且我也不關心散列函數和存儲桶(在Java的情況下)。

在哪些情況下我應該在TreeSet使用HashSet

HashSet 比 TreeSet 快得多(對於大多數操作,如添加、刪除和包含,常量時間與日志時間)但不提供像 TreeSet 那樣的排序保證。

哈希集

  • 該類為基本操作(添加、刪除、包含和大小)提供恆定的時間性能。
  • 它不保證元素的順序會隨着時間的推移保持不變
  • 迭代性能取決於HashSet的初始容量負載因子
    • 接受默認負載因子是非常安全的,但您可能希望指定一個初始容量,該容量大約是您期望集增長到的大小的兩倍。

樹集

  • 保證基本操作(添加、刪除和包含)的 log(n) 時間成本
  • 保證 set 的元素將被排序(升序、自然或您通過其構造函數指定的元素)(實現SortedSet
  • 不為迭代性能提供任何調整參數
  • 提供了一些方便的方法來處理有序集合,如first()last()headSet()tailSet()

要點:

  • 兩者都保證元素的無重復集合
  • 通常將元素添加到 HashSet 然后將集合轉換為 TreeSet 以進行無重復排序遍歷會更快。
  • 這些實現都不是同步的。 也就是說,如果多個線程同時訪問一個集合,並且至少有一個線程修改了該集合,則必須在外部進行同步。
  • LinkedHashSet在某種意義上介於HashSetTreeSet之間。 實現為一個帶有鏈表的哈希表,但是,它提供了插入順序迭代,這與 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               ║
╚══════════════╩═══════════════════════════════════════════════════════════════╝

HashSet是 O(1) 來訪問元素,所以它當然很重要。 但是不可能保持集合中對象的順序。

如果維護順序(根據值而不是插入順序)對您很重要,則TreeSet很有用。 但是,正如您所指出的,您正在交易訂單以獲得更慢的訪問元素的時間:基本操作的 O(log n)。

來自TreeSetjavadocs

此實現為基本操作( addremovecontains )提供有保證的 log(n) 時間成本。

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 他們在內部使用HashMapTreeMap所以你應該期待提到的Map的這種行為。

原來回答

消息編輯(完全重寫) 當順序無關緊要時。 兩者都應該給出 Log(n) - 看看其中一個是否比另一個快 5% 以上將是有用的。 HashSet 可以在循環中進行 O(1) 測試,應該揭示它是否是。

基於技術考慮,特別是性能方面的考慮,已經給出了很多答案。 在我看來,在TreeSetHashSet之間進行選擇很重要。

considerations first.但我更願意說選擇應該首先由考慮驅動。

如果對於您需要操作的對象,自然排序沒有意義,則不要使用TreeSet
它是一個有序集合,因為它實現了SortedSet 所以這意味着你需要覆蓋函數compareTo ,它應該與返回函數equals一致。 例如,如果您有一組名為 Student 的類的對象,那么我認為TreeSet沒有意義,因為學生之間沒有自然排序。 你可以按他們的平均成績來排序,好吧,但這不是“自然排序”。 函數compareTo不僅在兩個對象代表同一學生時返回 0,而且當兩個不同學生的成績相同時也會返回 0。 對於第二種情況, equals將返回 false(除非您決定在兩個不同的學生具有相同的成績時讓后者返回 true,這會使equals函數具有誤導性的含義,而不是說錯誤的含義。)
請注意, equalscompareTo之間的這種一致性是可選的,但強烈推薦。 否則接口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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM