• Java编程的逻辑 (44)


    本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html


    41节介绍了HashSet,我们提到,HashSet有一个重要局限,元素之间没有特定的顺序,我们还提到,Set接口还有另一个重要的实现类TreeSet,它是有序的,与HashSet和HashMap的关系一样,TreeSet是基于TreeMap的,上节我们介绍了TreeMap,本节我们来详细讨论TreeSet。

    下面,我们先来看TreeSet的用法,然后看实现原理,最后总结分析TreeSet的特点。

    基本用法

    构造方法

    TreeSet的基本构造方法有两个:

    public TreeSet()
    public TreeSet(Comparator<? super E> comparator)

    默认构造方法假定元素实现了Comparable接口,第二个使用传入的比较器,不要求元素实现Comparable。

    基本例子

    TreeSet经常也只是当做Set使用,只是希望迭代输出有序,如下面代码所示:

    Set<String> words = new TreeSet<String>();
    words.addAll(Arrays.asList(new String[]{
        "tree", "map", "hash", "map",     
    }));
    for(String w : words){
        System.out.print(w+" ");
    }

    输出为:

    hash map tree 

    TreeSet实现了两点:排重和有序。

    如果希望不同的排序,可以传递一个Comparator,如下所示:

    Set<String> words = new TreeSet<String>(new Comparator<String>(){
    
        @Override
        public int compare(String o1, String o2) {
            return o1.compareToIgnoreCase(o2);
        }});
    words.addAll(Arrays.asList(new String[]{
        "tree", "map", "hash", "Map",     
    }));
    System.out.println(words);

    忽略大小写进行比较,输出为:

    [hash, map, tree]

    需要注意的是,Set是排重的,排重是基于比较结果的,结果为0即视为相同,"map"和"Map"虽然不同,但比较结果为0,所以只会保留第一个元素。

    以上就是TreeSet的基本用法,简单易用。不过,因为有序,TreeSet还实现了NavigableSet和SortedSet接口,NavigableSet扩展了SortedSet,此外,TreeSet还有几个构造方法,我们来看下。

    高级用法

    SortedSet接口

    SortedSet接口与SortedMap接口类似,具体定义为:

    public interface SortedSet<E> extends Set<E> {
        Comparator<? super E> comparator();
        SortedSet<E> subSet(E fromElement, E toElement);
        SortedSet<E> headSet(E toElement);
        SortedSet<E> tailSet(E fromElement);
        E first();
        E last();
    }

    first()返回第一个元素,last()返回最后一个,headSet/tailSet/subSet都返回一个视图,包括原Set中的一定取值范围的元素,区别在于范围:

    • headSet:严格小于toElement的所有元素
    • tailSet: 大于等于fromElement的所有元素
    • subSet: 大于等于fromElement,且小于toElement的所有元素 

    与之前介绍的视图概念一样,对返回视图的操作会直接影响原Set。

    comparator()返回使用的比较器,如果没有自定义的比较器,返回值为null。

    我们来看一段简单的示例代码,以增强直观感受,输出用注释说明:

    SortedSet<String> set = new TreeSet<String>();
    set.addAll(Arrays.asList(new String[]{
        "c", "a", "b", "d","f"    
    }));
    
    System.out.println(set.first()); //a
    System.out.println(set.last()); //f
    System.out.println(set.headSet("b"));//[a]
    System.out.println(set.tailSet("d"));//[d, f]
    System.out.println(set.subSet("b", "e")); //[b, c, d]
    set.subSet("b", "e").clear(); //会从原set中删除
    System.out.println(set); //[a, f]

    NavigableSet接口

    与NavigableMap类似,NavigableSet接口扩展了SortedSet,主要增加了一些查找邻近元素的方法,比如:

    E floor(E e); //返回小于等于e的最大元素
    E lower(E e); // 返回小于e的最大元素
    E ceiling(E e); //返回大于等于e的最小元素
    E higher(E e); //返回大于e的最小元素

    相比SortedSet中的视图方法,NavigableSet增加了一些方法,以更为明确的方式指定返回值中是否包含边界值,如:

    NavigableSet<E> headSet(E toElement, boolean inclusive);
    NavigableSet<E> tailSet(E fromElement, boolean inclusive);
    NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                               E toElement,   boolean toInclusive);

    NavigableSet也增加了两个对头尾的操作:

    E pollFirst(); //返回并删除第一个元素
    E pollLast(); //返回并删除最后一个元素

    此外,NavigableSet还有如下方法,以方便逆序访问:

    NavigableSet<E> descendingSet();
    Iterator<E> descendingIterator();

    我们来看一段简单的示例代码,以增强直观感受,输出用注释说明:

    NavigableSet<String> set = new TreeSet<String>();
    set.addAll(Arrays.asList(new String[]{
        "c", "a", "b", "d","f"    
    }));
    System.out.println(set.floor("a")); //a
    System.out.println(set.lower("b")); //a
    System.out.println(set.ceiling("d"));//d
    System.out.println(set.higher("c"));//d
    System.out.println(set.subSet("b", true, "d", true)); //[b, c, d]
    System.out.println(set.pollFirst()); //a
    System.out.println(set.pollLast()); //f
    System.out.println(set.descendingSet()); //[d, c, b]

    其他构造方法

    TreeSet的其他构造方法为:

    public TreeSet(Collection<? extends E> c)
    public TreeSet(SortedSet<E> s)
    TreeSet(NavigableMap<E,Object> m) 

    前两个都是以一个已有的集合为参数,将其中的所有元素添加到当前TreeSet,区别在于,在第一个中,比较器为null,假定元素实现了Comparable接口,而第二个中,比较器设为和参数SortedSet中的一样。

    第三个不是public的,是内部用的。

    基本实现原理

    41节介绍过,HashSet是基于HashMap实现的,元素就是HashMap中的键,值是一个固定的值,TreeSet是类似的,它是基于TreeMap实现的,我们具体来看一下代码,先看其内部组成。

    内部组成

    TreeSet的内部有如下成员:

    private transient NavigableMap<E,Object> m;
    private static final Object PRESENT = new Object();

    m就是背后的那个TreeMap,这里用的是更为通用的接口类型NavigableMap,PRESENT就是那个固定的共享值。

    TreeSet的方法实现主要就是调用m的方法,我们具体来看下。

    构造方法

    几个构造方法的代码为:

    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }
    
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    
    public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }
    
    public TreeSet(SortedSet<E> s) {
        this(s.comparator());
        addAll(s);
    }

    代码都比较简单,就不解释了。

    添加元素

    add方法的代码为:

    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }

    就是调用map的put方法,元素e用作键,值就是固定值PRESENT,put返回null表示原来没有对应的键,添加成功了。

    检查是否包含元素

    代码为:

    public boolean contains(Object o) {
        return m.containsKey(o);
    }

    就是检查map中是否包含对应的键。

    删除元素

    代码为:

    public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }

    就是调用map的remove方法,返回值为PRESENT表示原来有对应的键且删除成功了。

    子集视图

    subSet方法的代码:

    public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                  E toElement,   boolean toInclusive) {
        return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                                       toElement,   toInclusive));
    }

    先调用subMap方法获取NavigatebleMap的子集,然后调用内部的TreeSet构造方法。

    头尾操作

    代码为:

    public E first() {
        return m.firstKey();
    }
    
    public E last() {
        return m.lastKey();
    }
    
     
    
    public E pollFirst() {
        Map.Entry<E,?> e = m.pollFirstEntry();
        return (e == null) ? null : e.getKey();
    }
    
    public E pollLast() {
        Map.Entry<E,?> e = m.pollLastEntry();
        return (e == null) ? null : e.getKey();
    }

    代码都比较简单,就不解释了。

    逆序遍历

    代码为:

    public Iterator<E> descendingIterator() {
        return m.descendingKeySet().iterator();
    }
    
    public NavigableSet<E> descendingSet() {
        return new TreeSet<>(m.descendingMap());
    }

    也很简单。

    实现原理小结

    TreeSet的实现代码都比较简单,主要就是调用内部NavigatableMap的方法。

    TreeSet特点分析

    与HashSet相比,TreeSet同样实现了Set接口,但内部基于TreeMap实现,而TreeMap基于大致平衡的排序二叉树 - 红黑树,这决定了它有如下特点:

    • 没有重复元素
    • 添加、删除元素、判断元素是否存在,效率比较高,为O(log2(N)),N为元素个数。
    • 有序,TreeSet同样实现了SortedSet和NavigatableSet接口,可以方便的根据顺序进行查找和操作,如第一个、最后一个、某一取值范围、某一值的邻近元素等。
    • 为了有序,TreeSet要求元素实现Comparable接口或通过构造方法提供一个Comparator对象。

    小结

    本节介绍了TreeSet的用法和实现原理,在用法方面,它实现了Set接口,但有序,同样实现了SortedSet和NavigatableSet接口,在内部实现上,它使用了TreeMap,代码比较简单。

    至此,我们已经介绍完了Java中主要常见的容器接口和实现类,接口主要有队列(Queue),双端队列(Deque),列表(List),Map和Set,实现类有ArrayList, LinkedList, HashMap, TreeMap, HashSet和TreeSet。

    关于接口Queue, Deque, Map和Set,Java容器类中还有其他一些实现类,它们各有特点,让我们在接下来的几节中继续探索。

    ---------------

    未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

  • 相关阅读:
    计算机网络 实验之 面向连接和无连接的套接字到底有什么区别?
    计算机网络 实验之 Internet 套接字
    计算机网络 实验之 socket是什么?套接字是什么?
    PepperLa's Boast(单调队列优化二维dp)
    理想的正方形(单调队列在二维的应用)
    移相器以及相控阵雷达移相器位数的选择
    盲速和频闪——雷达
    多普勒效应----雷达
    线性调频(LFM)脉冲压缩-----------雷达
    雷达----脉冲压缩
  • 原文地址:https://www.cnblogs.com/swiftma/p/5992093.html
Copyright © 2020-2023  润新知