해시 셋 vs 트리 셋


496

나는 항상 나무, 그 멋지고 O(n*log(n))단정함을 좋아 했습니다. 그러나 내가 아는 모든 소프트웨어 엔지니어는 내가 왜을 사용할지 물었다 TreeSet. CS 배경에서 나는 당신이 사용하는 모든 것이 중요하다고 생각하지 않으며 해시 함수와 버킷 ()의 경우 엉망이되지 않습니다 Java.

어떤 경우에 HashSet이상을 사용해야 TreeSet합니까?

답변:


860

HashSet은 TreeSet보다 훨씬 빠르지 만 (추가, 제거 및 포함과 같은 대부분의 작업에서 상수 시간 대 로그 시간) TreeSet과 같은 순서 보장은 제공하지 않습니다.

해시 세트

  • 이 클래스는 기본 작업 (추가, 제거, 포함 및 크기)에 대해 일정한 시간 성능을 제공합니다.
  • 요소의 순서가 시간이 지남에 따라 일정하게 유지된다고 보장하지는 않습니다.
  • 반복 성능은 초기 용량 과 HashSet 의 로드 팩터 에 따라 다릅니다 .
    • 기본로드 팩터를 수락하는 것이 안전하지만 세트가 커질 것으로 예상되는 크기의 약 두 배인 초기 용량을 지정할 수 있습니다.

TreeSet

  • 기본 작업에 대한 log (n) 시간 비용 보장 (추가, 제거 및 포함)
  • 집합의 요소가 오름차순, 자연적 또는 생성자를 통해 지정한 요소로 정렬되도록 보장합니다 (구현 SortedSet)
  • 반복 성능에 대한 튜닝 매개 변수를 제공하지 않습니다
  • 이벤트 몇 가지 편리한 방법은 같은 명령 세트를 처리하는 first(), last(), headSet(), 및 tailSet()

중요한 점 :

  • 두 요소 모두 중복없는 요소 수집을 보장합니다.
  • 일반적으로 HashSet에 요소를 추가 한 다음 복제되지 않은 정렬 된 순회를 위해 컬렉션을 TreeSet으로 변환하는 것이 더 빠릅니다.
  • 이러한 구현 중 어느 것도 동기화되지 않습니다. 즉, 여러 스레드가 동시에 세트에 액세스하고 스레드 중 하나 이상이 세트를 수정하는 경우 외부에서 동기화되어야합니다.
  • LinkedHashSet의는 어떤 의미에서의 중간입니다 HashSetTreeSet. 그러나 연결된 목록을 통해 실행되는 해시 테이블로 구현되지만 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

22
난 그냥이 벨라의 의견을 듣고 싶습니다. 시간 복잡도는 NOT 시간을 실행하는 것과 같은 일을하고, O (1) 항상 더 나은 O 이상 (2 ^ n)이 없습니다. 그 반대의 예는 요점을 보여줍니다. 10 개의 요소에 대해 1 조 개의 기계 명령어 (O (1))를 실행하는 데 사용되는 해시 알고리즘을 사용하는 해시 세트와 버블 정렬 (O (N ^ 2) avg / worst)의 일반적인 구현 . 버블 정렬은 매번 이길 것입니다. 요점은 알고리즘 클래스 모두가 시간 복잡도를 사용하여 근사치에 대해 생각하지만, 현실 세계에서 일정한 요인이 가르 칠 것입니다 문제가 자주.
Peter Oehlert

17
어쩌면 그것은 나뿐이지만 먼저 해시 세트에 모든 것을 추가 한 다음 끔찍한 트리 세트로 숨기라는 조언이 아닌가? 1) 해시 세트에 삽입하는 것은 데이터 세트의 크기를 미리 알고있는 경우에만 빠르며, 그렇지 않으면 O (n) 재 해시를 여러 번 지불합니다. 그리고 2) 어쨌든 세트를 변환 할 때 TreeSet 삽입 비용을 지불해야합니다. (해시 셋을 통한 반복은 대단히 효율적이지 않기 때문에 복수로)
TinkerTank

5
이 조언은 세트의 경우 항목을 추가하기 전에 항목이 중복인지 확인해야한다는 사실을 기반으로합니다. 따라서 트리 세트에 해시 세트를 사용하는 경우 중복을 제거하여 시간을 절약 할 수 있습니다. 그러나 비 중복 제품에 대한 두 번째 세트를 만드는 데 드는 비용을 고려할 때이 가격을 극복하고 시간을 절약하려면 중복 비율이 실제로 커야합니다. 물론 이것은 작은 세트의 경우 트리 세트가 해시 세트보다 빠를 수 있으므로 중간 및 큰 세트에 해당합니다.
SylvainL

5
@PeterOehlert : 벤치 마크를 제공하십시오. 나는 당신의 요점을 이해하지만 작은 세트 크기에서는 두 세트의 차이점이 거의 중요하지 않습니다. 그리고 집합이 구현이 중요한 지점으로 커지 자마자 log (n)은 문제가되고 있습니다. 일반적으로 리프를 찾고 / 액세스 / 추가 / 수정하는 몇 가지 캐시 미스 (거의 모든 액세스 레벨에 대해 거대한 트리에 있음)보다 빠른 해시 함수 (복잡한 것조차도) 크기가 더 빠릅니다. 적어도 그것은 Java 에서이 두 세트에 대한 나의 경험입니다.
Bouncner

38

아직 언급되지 않은 한 가지 장점 TreeSet은 "지역성 (locality)"이 더 크다는 것인데, 이는 (1) 두 항목이 순서대로 근처에 있으면 TreeSet데이터 구조에서 서로 가까이 배치되어 메모리에 배치된다. 그리고 (2)이 배치는 지역성의 원칙을 이용하는데, 이는 유사한 데이터가 종종 유사한 주파수를 가진 응용 프로그램에 의해 액세스된다고 말합니다.

이것은 대조적으로 HashSet 키와 상관없이 메모리 전체에 항목을 분산시키는와 입니다.

하드 드라이브에서 읽는 대기 시간 비용이 캐시 나 RAM에서 읽는 시간의 수천 배인 경우, 데이터가 실제로 로컬로 액세스되는 TreeSet경우 훨씬 더 나은 선택이 될 수 있습니다.


3
두 항목이 순서대로 근처에 있으면 TreeSet이 데이터 구조에서 서로 가까이 배치하여 메모리에 있음을 보여줄 수 있습니까 ?
David Soroko

6
Java와는 관련이 없습니다. 어쨌든 세트의 요소는 객체이며 다른 곳을 가리 키므로 많은 것을 저장하지 않습니다.
앤드류 갈라 쉬

Java에서 지역성이 부족하다는 일반적인 의견 외에도 OpenJDK의 TreeSet/ 구현은 지역적으로 TreeMap최적화되지 않았습니다. 레드-블랙 트리를 나타 내기 위해 차수 4의 b- 트리를 사용하여 지역 성과 캐시 성능을 향상시킬 수는 있지만 구현 방식이 아닙니다. 대신 각 노드는 TreeMap.EntryJDK 8 소스 코드 에서 알 수 있듯이 자체 키, 자체 값, 상위 및 왼쪽 및 오른쪽 하위 노드에 대한 포인터를 저장합니다 .
kbolino

25

HashSet요소에 액세스하려면 O (1)이므로 확실히 중요합니다. 그러나 세트에서 객체의 순서를 유지하는 것은 불가능합니다.

TreeSet순서를 유지하는 것이 중요합니다 (삽입 순서가 아닌 값으로). 그러나 앞에서 언급했듯이 기본 작업의 경우 요소에 액세스하는 데 시간이 오래 걸리는 주문을 거래하고 있습니다.

에 대한 javadocs에서TreeSet :

이 구현은 기본 작업 ( add, removecontains)에 대해 보장 된 log (n) 시간 비용을 제공합니다 .


22

1. HashSet은 null 객체를 허용합니다.

2. TreeSet은 null 객체를 허용하지 않습니다. 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의 첫 번째 객체로 null이 추가되면 TreeSet의 경우 제대로 작동합니다. 그리고 그 후에 추가 된 객체는 Comparator의 compareTo 메소드에서 NullPointerException을 발생시킵니다.
Shoaib Chikate

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);
Dávid Horváth

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 (log n) 대신 (평균) O (1)이기 때문입니다. 세트에 표준 항목이 포함되어 있으면 "해시 함수가없는 것"이 ​​아닙니다. 세트에 사용자 정의 클래스가 포함 된 경우, 유효 Java가 방법을 표시 하도록 구현 hashCode해야 HashSet하지만,를 사용하는 경우 TreeSet이를 작성 Comparable하거나을 제공해야합니다 Comparator. 클래스에 특정 순서가 없으면 문제가 될 수 있습니다.

나는 아주 작은 세트 / 맵 (<10 항목)에 때때로 TreeSet(또는 실제로 TreeMap) 사용했습니다. 하지만 실제로 얻는 것이 있는지 확인하지는 않았습니다. 큰 세트의 경우 그 차이가 상당 할 수 있습니다.

정렬이 필요한 TreeSet경우 업데이트가 자주 발생하고 정렬 된 결과가 자주 나타나지 않더라도 때때로 내용을 목록이나 배열에 복사하여 정렬하는 것이 더 빠를 수 있습니다.


10K 이상과 같은 큰 요소에 대한 모든 데이터 포인트
kuhajeyan

11

빈번한 재해시 (또는 HashSet의 크기를 조정할 수없는 경우 충돌)를 초래할 수있는 충분한 요소를 삽입하지 않으면 HashSet은 일정한 시간 액세스의 이점을 제공합니다. 그러나 많은 성장 또는 축소가있는 세트에서는 구현에 따라 실제로 Treeset으로 더 나은 성능을 얻을 수 있습니다.

메모리가 저에게 도움이된다면, 상각 된 시간은 기능적인 레드-블랙 트리로 O (1)에 가까울 수 있습니다. 오카 사키의 책은 내가 뽑을 수있는 것보다 더 나은 설명이 될 것이다. (또는 그의 출판물 목록 참조 )


7

물론 HashSet 구현은 훨씬 빠르며 순서가 없기 때문에 오버 헤드가 적습니다. Java에서 다양한 Set 구현에 대한 올바른 분석은 http://java.sun.com/docs/books/tutorial/collections/implementations/set.html 에서 제공됩니다 .

거기에 대한 논의는 또한 Tree vs Hash 문제에 대한 흥미로운 '중간'접근법을 지적합니다. Java는 "삽입 지향"링크 목록이있는 HashSet 인 LinkedHashSet을 제공합니다. 즉, 링크 된 목록의 마지막 요소도 가장 최근에 해시에 삽입됩니다. 이를 통해 TreeSet의 비용 증가없이 정렬되지 않은 해시의 무질서를 피할 수 있습니다.


4

TreeSet의이 두 정렬 된 모음 (다른 쪽의 트리 맵) 중 하나이다. Red-Black 트리 구조를 사용하지만 자연 순서에 따라 요소가 오름차순으로 정렬됩니다. 선택적으로 Comparable 또는 Comparator를 사용하여 요소의 클래스에 의해 정의 된 순서에 의존하지 않고 순서에 대한 고유 규칙을 컬렉션에 제공 할 수있는 생성자로 TreeSet을 구성 할 수 있습니다.

그리고 그것은 LinkedHashSet은 모든 요소에서 이중의 링크리스트를 보관 유지 HashSet의 정렬 된 버전입니다. 반복 순서를 염려 할 때 HashSet 대신이 클래스를 사용하십시오. HashSet을 반복하면 순서를 예측할 수 없지만 LinkedHashSet을 사용하면 요소를 삽입 한 순서대로 요소를 반복 할 수 있습니다


3

기술적 고려 사항, 특히 성능과 관련하여 많은 답변이 제공되었습니다. 나에 따르면, 선택 TreeSetHashSet문제 중 하나.

그러나 오히려 개념적인 고려 사항 에 의해 선택이 우선되어야한다고 말하고 싶습니다 .

조작해야 할 객체에 대해 자연스러운 순서가 의미가 없다면를 사용하지 마십시오 TreeSet.
구현하기 때문에 정렬 된 세트 SortedSet입니다. 따라서 함수를 재정의 compareTo해야하며 이는 함수를 반환하는 것과 일치해야합니다 equals. 예를 들어 Student라는 클래스의 객체 집합이 있다면TreeSet 학생들 사이에 자연스러운 순서가 없기 때문에 가 의미가 . 당신은 그들의 평균 등급으로 주문할 수 있습니다. 그러나 이것은 "자연적인 주문"이 아닙니다. 함수compareTo두 개체가 같은 학생을 나타낼 때뿐만 아니라 다른 두 학생이 같은 성적을 가질 때 0을 반환합니다. 두 번째 경우를 들어, equals(두 개의 다른 학생들이 할 것 같은 학년 때 진정한 후자의 반환을하기로 결정하지 않는 한 false를 반환 equals함수가 잘못된 의미를 말하고, 잘못된 의미가 없음.)
사이의 일관성을 유의하시기 바랍니다 equalscompareTo선택 사항이지만 강력히 권장됩니다. 그렇지 않으면 인터페이스 계약 Set이 깨져 코드가 다른 사람에게 오도되어 예기치 않은 동작이 발생할 수 있습니다.

링크 는이 질문에 관한 좋은 정보원이 될 수 있습니다.


3

오렌지를 먹을 수 있는데 왜 사과가 있습니까?

진심으로 남자와 여자-컬렉션이 크면 읽고 쓸데없이 쓰고, CPU 사이클을 지불하는 경우 컬렉션 선택은 더 나은 성능을 필요로하는 경우에만 관련이 있습니다. 그러나 대부분의 경우 이것은 실제로 중요하지 않습니다. 여기에는 몇 밀리 초가 걸리며 인간의 관점에서는 눈에 띄지 않습니다. 정말로 그렇게 중요하다면 어셈블러 나 C로 코드를 작성하지 않는 이유는 무엇입니까? [다른 토론을 큐]. 따라서 요점은 선택한 컬렉션을 사용하여 만족하면 문제를 해결하는 것입니다 (특히 작업에 가장 적합한 컬렉션 유형이 아니더라도). 소프트웨어는 가단성입니다. 필요한 경우 코드를 최적화하십시오. 밥 아저씨는 조기 최적화는 모든 악의 근원이라고 말합니다. 밥 아저씨가 그렇게 말합니다


1

메시지 편집 ( complete rewrite ) 순서가 중요하지 않은 때는 그때입니다. 둘 다 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

"어떤 경우에 TreeSet에서 HashSet을 사용하고 싶습니까?"
Austin Henley

1
내 요점은, 주문이 필요한 경우 모든 것을 HashSet에 넣고 그 HashSet을 기반으로 TreeSet을 만드는 것보다 TreeSet 만 사용하는 것이 좋습니다. 원래 게시물에서 HashSet + TreeSet 값이 전혀 표시되지 않습니다.
gli00001

@ gli00001 : 요점을 놓쳤다. 당신이하지 않으면 항상 정렬 할 요소 귀하의 설정이 필요하지만, 오히려 자주 조작하는거야, 대부분의 시간 빠른 작업에서 혜택에 HashSet의를 사용하는 가치에게 그것을 할 것입니다. 들어 가끔 당신이 순서로 요소를 처리하는 데 필요한 시간, 다음 단지 TreeSet에 함께 포장. 유스 케이스에 따라 다르지만 일반적인 유스 케이스는 아닙니다 (아마도 너무 많은 요소를 포함하지 않고 복잡한 순서 규칙이있는 세트를 가정합니다).
haylem
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.