橫掃Java Collections系列 —— TreeSet

GuoYaxiang發表於2019-02-24

介紹

簡言之,TreeSet是一個繼承AbstractSet類的有序集合類,實現了NavigableSet介面,該介面中提供了針對給定搜尋目標返回最接近匹配項的系列導航方法。主要有以下特點:

  • 其中儲存的元素具有唯一性
  • 不保證元素的插入順序
  • 對元素進行升序排序
  • 非執行緒安全

TreeSet中,元素按照其自然序升序排列和儲存,內部使用了一種自平衡二叉搜尋樹,也就是紅黑樹。紅黑樹作為自平衡二叉搜尋樹,其中每個節點都額外保有一個位元,用來指示當前的節點顏色是紅色或者黑色。這些“顏色”位元在後續的插入或者刪除中,有助於確保樹結構保持平衡。

建立TreeSet例項很簡單:

Set<String> treeSet = new TreeSet<>();
複製程式碼

此外,TreeSet還提供了一個有參構造器,可以傳入一個Comparable或者Comparator引數,該比較器會決定集合中元素排列的順序:

Set<String> treeSet = new TreeSet<>(Comparator.comparing(String::length));
複製程式碼

儘管TreeSet不是執行緒安全的容器,但是可以呼叫Collections.synchronizedSet()裝飾方法使其同步化:

Set<String> syncTreeSet = Collections.synchronizedSet(treeSet);
複製程式碼

常用方法

知道了如何建立TreeSet例項之後,接著看一下TreeSet中常用的操作。

add()

顧名思義,add()方法可以向TreeSet集合中新增元素,如果元素新增成功,則返回true,否則返回false。該方法約定,對於某元素而言,只有在集合中不存在相同元素時才可以新增。

讓我們向TreeSet中加入一個元素:

@Test
public void AddingElement() {
    Set<String> treeSet = new TreeSet<>();
 
    assertTrue(treeSet.add("String Added"));
 }
複製程式碼

add()方法非常重要,因為該方法的實現細節說明了TreeSet的內部實現原理,即利用TreeMapput方法來儲存元素:

public boolean add(E e) {
    return m.put(e, PRESENT) == null;
}
複製程式碼

程式碼中的變數m指向內部的一個TreeMap例項(注意TreeMap實現了NavigateableMap介面)。因此,當TreeSet內部依賴於一個NavigableMap,當建立一個TreeSet例項時,內部會通過一個TreeMap例項進行初始化:

public TreeSet() {
    this(new TreeMap<E,Object>());
}
複製程式碼

contains()

contain()方法可用於檢查給定TreeSet中是否包含某特定元素,如果包含則返回true,否則返回false

用法很簡單:

@Test
public void CheckingForElement() {
    Set<String> treeSetContains = new TreeSet<>();
    treeSetContains.add("String Added");
 
    assertTrue(treeSetContains.contains("String Added"));
}
複製程式碼

remove()

remove()方法用於刪除集合中的特定元素,如果集合中包含該特定元素,該方法會返回true

用法如下:

@Test
public void RemovingElement() {
    Set<String> removeFromTreeSet = new TreeSet<>();
    removeFromTreeSet.add("String Added");
 
    assertTrue(removeFromTreeSet.remove("String Added"));
}
複製程式碼

clear()

如果想要清除集合中的所有元素,可以使用clear()方法:

@Test
public void ClearingTreeSet() {
    Set<String> clearTreeSet = new TreeSet<>();
    clearTreeSet.add("String Added");
    clearTreeSet.clear();
  
    assertTrue(clearTreeSet.isEmpty());
}
複製程式碼

size()

size()方法可以得到TreeSet中元素的個數,該方法也是Set API中的基本方法之一:

@Test
public void CheckTheSizeOfTreeSet() {
    Set<String> treeSetSize = new TreeSet<>();
    treeSetSize.add("String Added");
  
    assertEquals(1, treeSetSize.size());
}
複製程式碼

isEmpty()

isEmpty()方法可用於驗證給定的TreeSet例項是否為空:

@Test
public void CheckEmptyTreeSet() {
    Set<String> emptyTreeSet = new TreeSet<>();
     
    assertTrue(emptyTreeSet.isEmpty());
}
複製程式碼

first()

如果TreeSet不為空,該方法會返回其中的第一個元素,否則會丟擲NoSUchElementException異常。示例如下:

@Test
public void GetFirstElement() {
    TreeSet<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    
    assertEquals("First", treeSet.first());
}
複製程式碼

last()

與上面的方法類似,如果TreeSet不為空,該方法將返回其中的最後一個元素:

@Test
public void GetLastElement() {
    TreeSet<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Last");
     
    assertEquals("Last", treeSet.last());
}
複製程式碼

subSet()

該方法接受fromElementtoElement兩個引數,並返回TreeeSet中這兩個引數指定索引範圍之間的所有元素。注意,該區間中包括fromElement,不包括toElement

@Test
public void UseSubSet() {
    SortedSet<Integer> treeSet = new TreeSet<>();
    treeSet.add(1);
    treeSet.add(2);
    treeSet.add(3);
    treeSet.add(4);
    treeSet.add(5);
    treeSet.add(6);
     
    Set<Integer> expectedSet = new TreeSet<>();
    expectedSet.add(2);
    expectedSet.add(3);
    expectedSet.add(4);
    expectedSet.add(5);
 
    Set<Integer> subSet = treeSet.subSet(2, 6);
  
    assertEquals(expectedSet, subSet);
}
複製程式碼

headSet()

該方法會返回TreeSet中小於指定項的所有元素:

@Test
public void UseHeadSet() {
    SortedSet<Integer> treeSet = new TreeSet<>();
    treeSet.add(1);
    treeSet.add(2);
    treeSet.add(3);
    treeSet.add(4);
    treeSet.add(5);
    treeSet.add(6);
 
    Set<Integer> subSet = treeSet.headSet(6);
  
    assertEquals(subSet, treeSet.subSet(1, 6));
}
複製程式碼

tailSet()

該方法返回TreeSet中大於或等於指定項的所有元素。

@Test
public void UseTailSet() {
    NavigableSet<Integer> treeSet = new TreeSet<>();
    treeSet.add(1);
    treeSet.add(2);
    treeSet.add(3);
    treeSet.add(4);
    treeSet.add(5);
    treeSet.add(6);
 
    Set<Integer> subSet = treeSet.tailSet(3);
  
    assertEquals(subSet, treeSet.subSet(3, true, 6, true));
}
複製程式碼

Iterator()

Iterator()方法會返回一個按照升序對集合中的元素進行迭代的迭代器,且該迭代器具有快速失敗機制。

升序迭代如下:

@Test
public void IterateTreeSetInAscendingOrder() {
    Set<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Second");
    treeSet.add("Third");
    Iterator<String> itr = treeSet.iterator();
    while (itr.hasNext()) {
        System.out.println(itr.next());
    }
}
複製程式碼

此外,TreeSet也允許進行降序迭代:

@Test
public void IterateTreeSetInDescendingOrder() {
    TreeSet<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Second");
    treeSet.add("Third");
    Iterator<String> itr = treeSet.descendingIterator();
    while (itr.hasNext()) {
        System.out.println(itr.next());
    }
}
複製程式碼

如果迭代器已經建立,並且集合被除迭代器的remove()方法之外的其它方式進行修改,迭代器將會丟擲ConcurrentModificationException

示例如下:

@Test(expected = ConcurrentModificationException.class)
public void ModifyingTreeSetWhileIterating() {
    Set<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Second");
    treeSet.add("Third");
    Iterator<String> itr = treeSet.iterator();
    while (itr.hasNext()) {
        itr.next();
        treeSet.remove("Second");
    }
}
複製程式碼

另外,如果使用迭代器的刪除方法,則不會丟擲異常:

@Test
public void RemovingElementUsingIterator() {
  
    Set<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Second");
    treeSet.add("Third");
    Iterator<String> itr = treeSet.iterator();
    while (itr.hasNext()) {
        String element = itr.next();
        if (element.equals("Second"))
           itr.remove();
    }
  
    assertEquals(2, treeSet.size());
}
複製程式碼

TreeSet無法對迭代器的快速失敗機制作出保證,因為在未同步的併發修改場景中,無法作出任何硬性保證。

Null元素的儲存

在Java 7之前,使用者可以向空TreeSet物件中新增null。但是,這個被當做了一個bug,因此在後續的版本中不再支援null值的新增。

當我們向TreeSet中新增元素時,其中的元素會按照自然序或者指定的comparator來進行排序。由於null不能與任何值作比較,因此當向TreeSet中新增null時,null與已有元素做比較時,會丟擲NullPointerException

@Test(expected = NullPointerException.class)
public void AddNullToNonEmptyTreeSet() {
    Set<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add(null);
}
複製程式碼

所有插入TreeSet中的元素要麼實現Comparable介面,要麼可以作為指定比較器的引數。這些元素之間可以互相比較,即e1.compareTo(e2)或者comparator.compare(e1,e2)都不會丟擲ClassCastException

class Element {
    private Integer id;
 
    // Other methods...
}
 
Comparator<Element> comparator = (ele1, ele2) -> {
    return ele1.getId().compareTo(ele2.getId());
};
 
@Test
public void UsingComparator() {
    Set<Element> treeSet = new TreeSet<>(comparator);
    Element ele1 = new Element();
    ele1.setId(100);
    Element ele2 = new Element();
    ele2.setId(200);
     
    treeSet.add(ele1);
    treeSet.add(ele2);
     
    System.out.println(treeSet);
}
複製程式碼

TreeSet效能

HashSet相比,TreeSet的效能稍低些。addremovesearch等操作時間複雜度為O(log n),按照儲存順序列印n個元素則耗時為O(n)

如果我們想要按序儲存條目,並且按照升序或者降序對集合進行訪問和遍歷,那麼TreeSet就應該作為首選集合。升序方式的操作與檢視效能要強於降序方式。

區域性性原則——是一個術語,表示根據記憶體訪問模式頻繁訪問相同值或者相關的儲存位置。

當我們說區域性性時,表明:

  • 相似的資料通常會被程式以相近的頻率訪問
  • 如果兩個條目按照給定順序接近,TreeSet會在資料結構中將這兩個元素放在相近的位置,記憶體中也同樣。

TreeSet作為一個有著更強區域性性特點的資料結構,我們可以根據區域性性原理得出結論,如果記憶體不足並且需要訪問自然順序相對接近的元素,那我們就應該優先考慮TreeSet。如果需要從硬碟中讀取資料,因為硬碟讀取的延時大大超過快取與記憶體讀取,因此TreeSet更加適合,因為其有著更好的區域性性。

相關文章