本系列文章经补充和完善,已修订整理成书《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编程及计算机技术的本质。用心原创,保留所有版权。

计算机程序的思维逻辑 (44) - 剖析TreeSet的更多相关文章

  1. 计算机程序的思维逻辑 (29) - 剖析String

    上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...

  2. 计算机程序的思维逻辑 (48) - 剖析ArrayDeque

    前面我们介绍了队列Queue的两个实现类LinkedList和PriorityQueue,LinkedList还实现了双端队列接口Deque,Java容器类中还有一个双端队列的实现类ArrayDequ ...

  3. 计算机程序的思维逻辑 (51) - 剖析EnumSet

    上节介绍了EnumMap,本节介绍同样针对枚举类型的Set接口的实现类EnumSet.与EnumMap类似,之所以会有一个专门的针对枚举类型的实现类,主要是因为它可以非常高效的实现Set接口. 之前介 ...

  4. 计算机程序的思维逻辑 (30) - 剖析StringBuilder

    上节介绍了String,提到如果字符串修改操作比较频繁,应该采用StringBuilder和StringBuffer类,这两个类的方法基本是完全一样的,它们的实现代码也几乎一样,唯一的不同就在于,St ...

  5. 计算机程序的思维逻辑 (31) - 剖析Arrays

    数组是存储多个同类型元素的基本数据结构,数组中的元素在内存连续存放,可以通过数组下标直接定位任意元素,相比我们在后续章节介绍的其他容器,效率非常高. 数组操作是计算机程序中的常见基本操作,Java中有 ...

  6. 计算机程序的思维逻辑 (53) - 剖析Collections - 算法

    之前几节介绍了各种具体容器类和抽象容器类,上节我们提到,Java中有一个类Collections,提供了很多针对容器接口的通用功能,这些功能都是以静态方法的方式提供的. 都有哪些功能呢?大概可以分为两 ...

  7. 计算机程序的思维逻辑 (38) - 剖析ArrayList

    从本节开始,我们探讨Java中的容器类,所谓容器,顾名思义就是容纳其他数据的,计算机课程中有一门课叫数据结构,可以粗略对应于Java中的容器类,我们不会介绍所有数据结构的内容,但会介绍Java中的主要 ...

  8. 计算机程序的思维逻辑 (40) - 剖析HashMap

    前面两节介绍了ArrayList和LinkedList,它们的一个共同特点是,查找元素的效率都比较低,都需要逐个进行比较,本节介绍HashMap,它的查找效率则要高的多,HashMap是什么?怎么用? ...

  9. 计算机程序的思维逻辑 (41) - 剖析HashSet

    上节介绍了HashMap,提到了Set接口,Map接口的两个方法keySet和entrySet返回的都是Set,本节,我们来看Set接口的一个重要实现类HashSet. 与HashMap类似,字面上看 ...

随机推荐

  1. 前端框架 EasyUI (0) 重新温习(序言)

    几年前,参与过一个项目.那算是一个小型的信息管理系统,BS 结构的,前端用的是基于 jQuery 的 EasyUI 框架. 我进 Team 的时候,项目已经进入开发阶段半个多月了.听说整个项目的框架是 ...

  2. C++中的事件分发

    本文意在展现一个C++实现的通用事件分发系统,能够灵活的处理各种事件.对于事件处理函数的注册,希望既能注册到普通函数,注册到事件处理类,也能注册到任意类的成员函数.这样在游戏客户端的逻辑处理中,可以非 ...

  3. C语言 · 4_2找公倍数

    问题描述 这里写问题描述. 打印出1-1000所有11和17的公倍数. 样例输入 一个满足题目要求的输入范例.例:无 样例输出 与上面的样例输入对应的输出.例:   代码如下: #include< ...

  4. .NET 基础 一步步 一幕幕[面向对象之方法、方法的重载、方法的重写、方法的递归]

    方法.方法的重载.方法的重写.方法的递归 方法: 将一堆代码进行重用的一种机制. 语法: [访问修饰符] 返回类型 <方法名>(参数列表){ 方法主体: } 返回值类型:如果不需要写返回值 ...

  5. php报错 ----> Call to undefined function imagecreatetruecolor()

    刚才在写验证码的时候,发现报错,然后排查分析了一下,原来是所用的php版本(PHP/5.3.13)没有开启此扩展功能. 进入php.ini 找到extension=php_gd2.dll ,将其前面的 ...

  6. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  7. 介绍一款原创的四则运算算式生成器:CalculateIt2

    家里小朋友读一年级了,最近每天都有一些10以内的加减法口算练习,作为程序员爸爸,自然也是想办法能够偷懒,让电脑出题,给小朋友做些练习.于是,自己在业余时间开发了一个四则运算算式生成器,名为:Calcu ...

  8. Android快乐贪吃蛇游戏实战项目开发教程-04虚拟方向键(三)三角形按钮效果

    该系列教程概述与目录:http://www.cnblogs.com/chengyujia/p/5787111.html 一.知识点讲解 当我们点击系统自带的按钮时,按钮的外观会发生变化.上篇博文中我们 ...

  9. BZOJ 3504: [Cqoi2014]危桥 [最大流]

    3504: [Cqoi2014]危桥 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 1407  Solved: 703[Submit][Status] ...

  10. Spring Quartz实现任务调度

    任务调度 在企业级应用中,经常会制定一些"计划任务",即在某个时间点做某件事情 核心是以时间为关注点,即在一个特定的时间点,系统执行指定的一个操作 任务调度涉及多线程并发.线程池维 ...