散列集(HashSet)與樹集(TreeSet)java
經常使用的集合類型
數組
經常使用集合類型 |
描述 |
ArrayList |
一種能夠動態增加和縮減的索引序列 |
LinkedList |
一種能夠在任何位置進行高效地插入和刪除操做的有序序列 |
ArrayDeque |
一種循環數組實現的雙端隊列 |
HashSet |
一種沒有重複元素的無序集合 |
TreeSet |
一種有序集 |
EnumSet |
一種包含枚舉類型值的集 |
LinkedHashSet |
一種能夠記住元素插入次序的集 |
PriorityQueue |
一種容許高效刪除最小元素的集合 |
HashMap |
一種存儲健值關聯的數據結構 |
TreeMap |
一種健值有序排列的映射表 |
LinkedHashMap |
一種能夠記住健值項添加次序的映射表 |
WeakHashMap |
一種其值無用武之地後能夠被垃圾回收器回收的映射表 |
IdentityHashMap | 一種用==,而不使用equals比較健值的映射 |
鏈表和數組能夠按意願排列元素的次序,可是若是想要查看某個元素,又不知道或者忘記了它的位置,就須要訪問全部元素,直到找到爲止。若是集合中包含的元素 不少,將會消耗不少時間。若是不在乎元素的次序,能夠有幾種可以快速查找元素的數據結構。其缺點就是沒法控制元素的出現次序,由於這些數據結構會按照有利 於其操做目的的原則組織數據。數據結構
有一種數據結構能夠快速的查找全部須要的對象,就是散列表(hashtable)。散列表爲每一個對象計算一個整數,稱爲散列碼(hashcode)。散列碼是由對象的實例域產生的一個整數。更準確地說,具備不一樣數據域的對象將產生不一樣的散列碼。ide
上圖列出了幾個String對象散列碼的實例,若是是自定義的類,就要負責實現這個類的hashCode方法。hashCode方法應該與equals方法兼容,即若是a.equals(b)爲true,那麼a與b必須有相同的散列碼。函數
在 java中,散列表用鏈表數組實現。每一個列表被稱爲桶(bucket)。想要查找表中對象的位置,須要用對象的散列碼與桶的總數取餘,例如上圖對象str 的散列碼是3179,加入集合的桶數是128,那麼str將在(3179除以128餘107)107號桶中。有時桶已經被佔滿,也是不可避免的。這種狀況 被稱之爲散列衝突(hash collision)。這時,須要用新對象與桶中的全部對象進行比較,查看這個對象是否已經存在。若是散列碼是合理且隨機分佈的,桶的數目也足夠大,須要 比較的次數就會不多。性能
若是想要更多地控制散列表的運行性能,能夠指定一個初始的桶數。一般,將桶數設置爲預計元素個數的75%~150%。有些研究人員認爲:儘管尚未確鑿的證據,但最好將桶數設置爲一個素數,以防鍵的彙集。測試
當 然,並非總能知道須要存儲多少個元素,也有可能最初的估計太低。若是散列表太滿,就須要再散列(rehashed)。若是對散列表再散列,就須要建立一 個新的桶數更多的散列表,並將全部元素插入到這個新表中,而後丟棄原來的表。填裝因子(load factor)決定什麼時候對散列表進行再散列。例如,若是填裝因子是0.75(默認值),而散列表中超過75%的位置已經填入元素,這個散列表就會用雙倍的 桶數自動地進行再散列。對於大多數程序來講,75%是比較合理的填裝因子數。this
Java提供了一個HashSet類,它基於的就是散 列表的集。能夠用add方法添加元素,若是元素不存在,就能夠添加到集合。同時,contains方法已經被從新定義,用來快速地查看是否某個元素已經出 如今集中。它只根據桶來查找元素,沒必要查看集合中全部的元素。spa
散列集迭代器將依次訪問全部的桶。因爲散列將元素分散在表的各個位置上,因此訪問它們的順序幾乎是隨機的。只有不關心集合中元素的順序時才應該使用hashset類。code
下面作一個測試,從50萬行數據中讀取每一行並保存到hashset中,文件中每行會重複5次,最終會獲得一個有10萬個元素的hashset:
@Test public void SetTest() throws Exception { HashSet<String> words = new HashSet<String>(); // 從文件中讀取每一行,添加到hashset中。 String encoding = "GBK"; int maxline = 0; File file = new File("D://word.txt"); if (file.isFile() && file.exists()) { InputStreamReader read = new InputStreamReader(new FileInputStream( file), encoding); BufferedReader bufferedReader = new BufferedReader(read); String lineTxt = null; while ((lineTxt = bufferedReader.readLine()) != null) { words.add(lineTxt); maxline++; } read.close(); } else System.out.println("no file"); System.out.println("word.txt文檔一共有行數:" + maxline); System.out.println("========================"); // 訪問hashset中的數據。 int sum = 0; Iterator<String> iterator = words.iterator(); while (iterator.hasNext()) { String str = iterator.next(); if(sum < 20) { System.out.println(str); } sum++; } System.out.println(". . . "); System.out.println("hashset中共有元素" + sum + "個"); }
能夠看到重複的數據並無保存到hashset中,並且元素的位置是隨機的。在更改集中的元素時要格外當心。若是元素的散列碼發生了變化,元素在數據結構中的位置也會發生改變。
TreeSet類與散列集十分相似,不過它比散列集有所改進。TreeSet是一個有序集合(sorted collection)。能夠以任意順序將元素插入到集合中。在對集合進行便利時,每一個值將自動按照排序後的順序呈現。
@Test public void TreeSetTest() { TreeSet<String> treeSet = new TreeSet<String>(); treeSet.add("Bob"); treeSet.add("Amy"); treeSet.add("Carl"); for(String s : treeSet) System.out.println(s); } /** * output * Amy * Bob * Carl * */
能夠看到將字符串添加到treeset中,輸出時按照字符串的排序進行了打印,字母A在B以前。TreeSet類的排序是用樹結構完成的(當前實現使用的是紅黑樹red - black - tree)。每次將一個元素添加到樹中,都會被放置在正確的排序位置上。所以,迭代器老是以排好序的順序訪問每一個元素。
講一個元素添加到樹中要比添加到散列表中慢,可是,與元素添加到數組或鏈表的正確位置上相比仍是要快不少的。若是樹中包涵n個元素,查找新元素的正確位置平均要log2n次比較。例如一棵樹包含了1000個元素,添加一個新元素大約須要比較10次。
TreeSet在默認狀況下,假定插入的元素實現了Comparable接口。也就是說,若是a與b相等,調用a.compareTo(b)必定返回0;若是a排序在b以前,則返回負數;若是a位於b以後,則返回正值。具體返回什麼值並不重要,關鍵是符號(>0、0或<0)。
然而,使用Comparable接口定義排序顯然尤爲侷限性。對於一個給定的類,只可以實現這個接口一次。若是在一個集合中須要經過一個優先級字段排序,而在另外一個集合中卻要按照名稱排序該怎麼辦呢?另外,若是須要對一個類的對象進行排序,而這個累的建立者又沒有實現Comparable接口,又該怎麼辦呢?
這種狀況下,能夠經過將Comparator對象傳遞給TreeSet構造器來告訴TreeSet使用不一樣的比較方法。
寫一段代碼來測試一下:
public class Task implements Comparable<Task> { public Task() { } public Task(String name, int priority) { super(); this.name = name; this.priority = priority; } @Override public int hashCode() { return 13 * name.hashCode() + 17 * priority; } @Override public boolean equals(Object obj) { if(this == obj) return true; if(obj == null) return false; if(getClass() != obj.getClass()) return false; Task t = (Task)obj; return name.equals(t.getName()) && priority == t.getPriority(); } public int compareTo(Task t) { return priority - t.priority; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPriority() { return priority; } public void setPriority(int priority) { this.priority = priority; } private String name; private int priority; }
上面這個類實現了Comparator接口,使用的是對象的優先級字段進行排序。下面將這個對象放到TreeSet中。
@Test public void TreeSetTest() throws Exception { // 經過對象實現的compareTo方法進行排序。 TreeSet<Task> treeSet = new TreeSet<Task>(); treeSet.add(new Task("task3", 2)); treeSet.add(new Task("task1", 3)); treeSet.add(new Task("task2", 1)); for (Task t : treeSet) System.out.println(t.getName() + "," + t.getPriority()); // 還能夠經過建立集合時指定的方式進行排序。 TreeSet<Task> treeSetByName = new TreeSet<Task>(new Comparator<Task>(){ @Override public int compare(Task o1, Task o2) { return o1.getName().compareTo(o2.getName()); } }); System.out.println(); treeSetByName.addAll(treeSet); for (Task t : treeSetByName) System.out.println(t.getName() + "," + t.getPriority()); }
第一個treeSet對象使用Task對象實現的比較方法進行排序,輸出以下:
而第二個treeSetByName對象使用了在構造時傳入的比較方法去進行排序,傳入了經過名稱來排序,輸入結果以下:
如今考慮一個問題,是否老是應該用treeset取代hashset呢?畢竟,treeset添加一個元素所花費的時候看上去並不長,並且元素是自動排序的。
到底應該怎樣作將取決於索要收集的數據。
若是不須要對數據進行排序,就沒有必要付出排序的開銷。更重要的是,對於某些數據來講,對其排序要比散列函數更加困難。散列函數只是將對象適當地打亂順序存放,而比較卻要精確地判別每一個對象。
另外,若是使用TreeSet,在集合中存放矩形(rectangle),該如何比較兩個矩形呢?比較面積行不通,可能有兩個長寬不等的矩形,他們的座標不一樣,但面積卻相同。
樹的排序必須是總體排序,也就是說,任意兩個元素必須是可比的,而且只有在兩個元素相等時結果才爲0。