更好的使用Java集合(二)

    散列集(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。

相關文章
相關標籤/搜索