更好的使用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。

相关文章
相关标签/搜索