Java基础 集合框架 之Set框架之TreeSet

TreeSet

  • TreeSet 数据结构及实现原理
  • TreeSet的构造方法
  • TreeSet 核心特性
    • 有序性(`排序大小输出`)
      • 自然排序
      • 定制排序
    • 唯一性
    • 底层数据结构:红黑树
    • 导航方法(特色核心优势)
      • 基础导航方法
      • 范围视图(不修改原集合)
      • 提取和删除元素
      • 逆序视图
    • 不允许null元素
    • TreeSet 线程不安全
      • TreeSet线程不安全体现
      • 解决方案
  • TreeSet优缺点
  • TreeSet 应用场景

类结构传承去区别于HashSet实现了set接口,TreeSet 并没有直接实现set接口

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable

但是TreeSet实现了NavigableSet接口,这个NavigableSet接口扩展了SortedSet接口,最大的特色就是导航方法,它是基于 红黑树 数据结构实现的,底层具体实现是基于TreeMap

TreeSet 数据结构及实现原理

底层逻辑是基于TreeMap对红黑树的管理操作方式,TreeMap详情介绍,新增和删除元素方式就不阐述了,可以参考TreeMap的机制,这里单独简单介绍下TreeSet的有序性 底层逻辑

	// 直接输出TreeSet集合会输出一个排序的集合
	 TreeSet<Integer> treeSet = new TreeSet<>();
        treeSet.add(80);
        treeSet.add(90);
        System.out.println(treeSet);//[80, 90]
	
	//这是因为TreeSet会自动维护一个有序的集合(当然比起无序的HashSet这是额外的开销)
	//调用链:iterator() → navigableKeySet().iterator()
   public Iterator<E> iterator() {
        return m.navigableKeySet().iterator();
    }
    //本质是调用TreeMap.navigableKeySet() 获取key键的合集
    // 而KeyIterator 是 TreeMap 的内部类,实现了中序遍历逻辑 从而获取到正序输出Key键值合集
	public Iterator<K> iterator() {
    return new KeyIterator(getFirstEntry()); 
}

TreeSet的构造方法

无参构造(自然排序)

//要求元素实现 Comparable 接口(如 Integer, String),适用于实现了Comparable 接口的类
TreeSet<Integer> treeSet= new TreeSet<>();

自定义比较器构造

// 降序排序
TreeSet<Integer> set2 = new TreeSet<>((a, b) -> b - a);
// 按字符串长度排序
TreeSet<String> set3 = new TreeSet<>(Comparator.comparing(String::length));

从集合初始化

List<Integer> list = Arrays.asList(5, 2, 8, 1);
TreeSet<Integer> set4 = new TreeSet<>(list);  // 自动排序为 [1,2,5,8]

从有序集合复制

SortedSet<Integer> sortedSet = new TreeSet<>(Arrays.asList(9, 4, 7));
TreeSet<Integer> set5 = new TreeSet<>(sortedSet);  // 继承排序规则

TreeSet 核心特性

有序性(排序大小输出)

TreeSet 最核心的特点就是它会自动对其包含的元素进行排序,排序方式分为自然排序(Comparable接口)和定制排序(Comparator)两种方式

自然排序

如果创建 TreeSet 时没有显式提供Comparator,则要求集合中的元素必须实现 Comparable 接口

	//比如使用的是默认的构造方法
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    //而不是使用带有器的构造方法 构建的TreeSet
        public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

TreeSet 会调用元素的 compareTo(Object o) 方法来确定元素的顺序,例如:String (按字典序)、Integer, Double, Date 等标准类都实现了 Comparable,所以适用于常见包装类(既已实现了Comparable的类)

TreeSet<String> words = new TreeSet<>();
words.add("dog");
words.add("apple");
words.add("cat");
// 遍历输出: apple, cat, dog (按字母顺序升序)

定制排序

在创建 TreeSet 时,可以传入一个实现了Comparator 接口的对象

    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

TreeSet 会使用这个 Comparator compare(Object o1, Object o2)方法来比较元素并确定顺序,
1.在Comparator 比较方法里面,我们可以自定义比较规则,所以叫比较排序
2.比较规则上优先级选择上 定制排序 大于 自然排序(即使元素实现了 Comparable)

		// 添加元素时候会先判断定制排序是否存在
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {

示例 (按字符串长度排序,长度相同则按字母顺序):

Comparator<String> byLengthThenNatural = Comparator
    .comparingInt(String::length) // 先按长度
    .thenComparing(Comparator.naturalOrder()); // 长度相同再按自然顺序
TreeSet<String> words = new TreeSet<>(byLengthThenNatural);
words.add("dog");
words.add("apple");
words.add("cat");
words.add("ant");
// 遍历输出: dog, ant, cat, apple (长度:3, 3, 3, 5; 长度相同的按字母顺序: ant, cat, dog)

唯一性

和 HashSet 一样不允许存储重复的元素。尝试添加重复元素会被忽略(add 方法返回 false)

        Set<String> treeSet = new TreeSet<>();
        System.out.println(treeSet.add("a"));// true
        System.out.println(treeSet.add("a")); //false

底层实现原理

	//treeSet.add方法底层实现是调用TreeMap新增节点方法,然后根据返回值判断
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    // TreeMap数据结构 红黑树,没有重复键值的添加成功返回null,有相同的更新并返回旧的值
	public V put(K key, V value) {}

	TreeMap<String,String> map = new TreeMap<>();
    System.out.println(map.put("1","FirstValue")); // 添加成功返回null
    System.out.println(map.put("1","NextValue"));  // 更新并返回旧的值FirstValue

底层数据结构:红黑树

TreeSet 内部使用红黑树数据结构来存储元素,TreeSet 底层具体实现是基于TreeMap,而TreeMap的底层树结构是红黑树

红黑树是一种自平衡的二叉查找树。这种结构保证了 TreeSet 的基本操作添加 add()、删除 remove()、查找 contains())的时间复杂度都是 O(log n)自平衡的二叉查找树设计核心目标就是防止二叉查找树退化成链表(复杂度 O(n))

导航方法(特色核心优势)

TreeSet 实现了NavigableSet接口 丰富的导航方法,可方便地查找与给定元素“最接近”的元素

    public static void main(String[] args) {
        TreeSet<Integer> treeSet = new TreeSet<>();
        treeSet.add(80);
        treeSet.add(90);
        treeSet.add(70);
        treeSet.add(50);
        treeSet.add(60);
        treeSet.add(40);
        // 原始集合: [40, 50, 60, 70, 80, 90]
        System.out.println("原始集合: " + treeSet); 
    }

首先TreeSet元素的大小输出是按照排序输出的

基础导航方法

        // 1. 基础导航方法
        System.out.println("\n=== 基础导航方法 ===");
        System.out.println("第一个元素: " + treeSet.first());  // 40
        System.out.println("最后一个元素: " + treeSet.last());       // 90
        System.out.println("大于60的最小元素: " + treeSet.higher(60));  // 70
        System.out.println("小于80的最大元素: " + treeSet.lower(80));   // 70
        System.out.println("大于等于70的最小元素: " + treeSet.ceiling(70));  // 70
        System.out.println("小于等于70的最大元素: " + treeSet.floor(70));    // 70

范围视图(不修改原集合)

        System.out.println("\n=== 范围视图 ===");
        // 小于范围
        System.out.println("头视图(严格小于80): " + treeSet.headSet(80));
        System.out.println("头视图(小于等于80): " + treeSet.headSet(80, true));
        // 大于范围
        System.out.println("尾视图(大于等于80): " + treeSet.tailSet(80));
        System.out.println("尾视图(严格大于80): " + treeSet.tailSet(80, false));
        // 子范围
        System.out.println("子视图[60-90]: " + treeSet.subSet(60, 90));
        System.out.println("子视图(60-90[含边界90]): " +
                treeSet.subSet(60, true, 92, true));

打印输出

=== 范围视图 ===
头视图(严格小于80): [40, 50, 60, 70]
头视图(小于等于80): [40, 50, 60, 70, 80]
尾视图(大于等于80): [80, 90]
尾视图(严格大于80): [90]
子视图[60-90]: [60, 70, 80]
子视图(60-90[含边界90]): [60, 70, 80, 90]

提取和删除元素

        // 3. 提取和删除元素
        System.out.println("\n=== 元素操作 ===");
        System.out.println("删除最小元素并返回: " + treeSet.pollFirst()); // 40
        System.out.println("删除最大元素并返回: " + treeSet.pollLast());  // 90
        System.out.println("当前集合: " + treeSet); // [50, 60, 70, 80]

逆序视图

        // 4. 逆序视图
        System.out.println("\n=== 逆序操作 ===");
        NavigableSet<Integer> descendingScores = treeSet.descendingSet();
        System.out.println("逆序集合: " + descendingScores); // [80, 70, 60, 50]
        System.out.println("逆序集合的第一个元素: " + descendingScores.first());  // 80
        System.out.println("逆序集合的最后一个元素: " + descendingScores.last()); // 50

不允许null元素

TreeSet不允许null值区别于HashSet的允许一个null值

这是因为TreeSet底层调用的是TreeMap,底层数据是二叉搜索树:任意节点都比左子节点大 比右节点小,新增数据需要比较查找合适的插入位置,null无法比较大小会抛出空指针的,除非在定制排序Comparator 比较器显式的处理null 比如把null作为特殊的最大或最小值存在树中

HashSet的底层调用的是HashMap,底层数据是 哈希桶+(桶内数据) 桶内数据结构节点小于等于6是链表,大于等于8是红黑树 ,如果是存在链表里面 键大小多少是不需要比较的没有影响,所以HashMap对键值为null的节点特殊处理

键为null的条目,被固定存储在数组索引0的桶bucket(即table[0]),始终以链表节点 Node 形式存储,永远不会转换为树节点 TreeNode

TreeSet 线程不安全

TreeSet底层实现是依靠TreeMap,而TreeMap 本身非线程安全,其内部的红黑树结构在并发修改时极易破坏平衡性

TreeSet线程不安全体现

情况1:多线程同时添加元素

TreeSet<Integer> set = new TreeSet<>();
// 线程A
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        set.add(i);
    }
}).start();
// 线程B
new Thread(() -> {
    for (int i = 1000; i < 2000; i++) {
        set.add(i);
    }
}).start();
//可能结果:
// 部分元素丢失(最终 size < 2000)
// 红黑树结构破坏导致无限循环
// 抛出 ClassCastException(树节点类型损坏)

情况2:读写并发(迭代时修改)

TreeSet<String> names = new TreeSet<>(Arrays.asList("Alice", "Bob"));
// 读线程
new Thread(() -> {
    for (String name : names) { // 迭代过程中
        System.out.println(name);
        Thread.sleep(100); // 模拟处理耗时
    }
}).start();
// 写线程
new Thread(() -> {
    names.add("Carol"); // 并发修改
}).start();
//可能结果:
// ConcurrentModificationException
// 迭代结果不一致(看到部分修改)
// 无限循环或程序卡死

解决方案

方案一:使用Collections.synchronizedSortedSet包装(粗粒度锁)
实现原理:通过包装器在所有方法上加 synchronized 锁

SortedSet<Integer> safeSet = Collections.synchronizedSortedSet(
    new TreeSet<Integer>()
);

// 写操作(自动加锁)
safeSet.add(42);

// 读操作(需要手动同步)
synchronized (safeSet) {
    for (Integer num : safeSet) {
        System.out.println(num);
    }
}
//适用场景:
//写操作频繁程度中等
//对吞吐量要求不高
//需要保持 TreeSet 的有序特性

方案二:使用并发集合 ConcurrentSkipListSet(最佳实践)
ConcurrentSkipListSet:本质上是依靠 ConcurrentSkipListMap 实现的
1.线程安全:ConcurrentSkipListSet线程安全,可在多线程环境中安全使用,而不需额外的同步措施
2.实现原理:底层使用跳表(Skip List)数据结构,这使得它在并发环境下具有较好的性能
3.有序性:根据元素的自然顺序或者通过构造函数提供的Comparator进行排序
4.唯一性:作为一个Set,它不允许重复元素

// 创建并发安全的有序集合
ConcurrentSkipListSet<Integer> concurrentSet = 
    new ConcurrentSkipListSet<>();

// 多线程安全操作
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
    executor.submit(() -> {
        for (int j = 0; j < 1000; j++) {
            concurrentSet.add(ThreadLocalRandom.current().nextInt());
        }
    });
}

技术优势:
1.无锁算法:使用(内置无锁)CAS 操作实现并发控制
2.高吞吐量:平均 O(log n) 时间复杂度
3.弱一致性迭代器:迭代时可能反映其他线程的并发修改,不抛 ConcurrentModificationException
4.完全并发:支持读写并行

跳表结构示意图:
跳表是一种多层的有序链表,最底层(第0层)包含所有元素,每一层都是下一层的子集。上层相当于下层的“快速通道”,通过增加多层索引来提高查找效率

假设有一个包含以下整数的跳表:1, 4, 7, 9, 12, 15, 18, 20。构建一个跳表,它可能有如下结构
最高层(比如第3层):只包含极少数元素,比如头节点和9(假设随机层数分配后9有3层)。
第2层:包含头节点、7、9、20(假设这些节点被分配到了第2层)。
第1层:包含头节点、4、7、9、15、20。
第0层(最底层):包含所有元素:1、4、7、9、12、15、18、20。

Level 3: head  -------------------------------------------> 9 -------> null
Level 2: head  ---------------------------> 7 -------> 9 -------> 20 -----> null
Level 1: head -------> 4 -------> 7 -------> 9 -------> 15 -----> 20 -----> null
Level 0: head -> 1 -> 4 -> 7 -> 9 -> 12 -> 15 -> 18 -> 20 -> null

说明:
1.每一层都是一个有序链表
2.每个节点在插入时随机决定其层数(比如抛硬币,直到出现反面为止,层数即连续正面的次数),因此每个节点出现在第0层,然后有1/2的概率出现在第1层,1/4的概率出现在第2层,以此类推
3.查找时从最高层开始,如果当前节点的下一个节点小于目标值,则继续向右;否则,下降一层继续查找。这样就能跳过大量节点,提高查找效率

例如,查找18:
   - 从第3层开始:head->9,18>9,所以向右到9,但9后面是null(第3层结束),下降到第2层。
   - 第2层:9后面是20,18<20,所以下降到第1层。
   - 第1层:9后面是15,18>15,所以向右到15;15后面是20,18<20,所以下降到第0层。
   - 第0层:15后面是18,找到

 插入和删除操作需要调整前后节点的指针,并且由于是并发的,使用CAS操作来保证线程安全。
 通过这种多层结构,跳表可以在平均情况下达到O(log n)的查找、插入和删除时间复杂度,并且支持高并发

当然 劣势就是需要每次新增或者删除之后 额外维护这些多层链表的指针,已空间换时间 和 安全

方案三: 显式锁控制(精细控制)(不推荐)
适用场景:需要特殊同步逻辑或使用 TreeSet 特定功能

class SafeTreeSet<E> {
    private final TreeSet<E> set = new TreeSet<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void add(E e) {
        lock.writeLock().lock();
        try {
            set.add(e);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public E first() {
        lock.readLock().lock();
        try {
            return set.first();
        } finally {
            lock.readLock().unlock();
        }
    }
    
    // 安全遍历方法
    public void safeTraverse(Consumer<E> action) {
        lock.readLock().lock();
        try {
            for (E e : set) {
                action.accept(e);
            }
        } finally {
            lock.readLock().unlock();
        }
    }
}

锁选择策略

锁类型 适用场景 注意事项
synchronized 简单同步需求 自动释放,可能死锁
ReentrantLock 需要超时或可中断锁 必须手动释放
ReentrantReadWriteLock 读多写少场景 写锁降级需谨慎
StampedLock 极高并发读场景 非重入锁,API更复杂

TreeSet优缺点

优点:
1.自动排序: 最大的优势,元素总是有序的
2.高效查找/插入/删除 (平均和最坏情况): 基于红黑树,核心操作的时间复杂度为 O(log n)
3.范围查询高效: 利用有序性和导航方法,查找子集、首尾元素等操作非常高效。

缺点:
1.比 HashSet 慢 因为需要维护排序,添加、删除和查找操作通常比 HashSet (平均 O(1)) 慢一些
2.不允许 null 元素: 如果尝试添加 null 元素,会抛出 NullPointerException (除非使用的 Comparator 显式处理了 null)
3.不是线程安全的: 和大多数集合类一样,TreeSet 不是线程安全的。如果需要在多线程环境中使用,需要使用 Collections.synchronizedSortedSet 进行包装,或者在外部进行同步控制

TreeSet 应用场景

1.需要一个元素唯一且自动排序的集合
2.需要频繁地进行范围查询(如查找某个区间内的元素)或访问首尾元素(最小/最大值)时
3.需要按非自然顺序(自定义顺序)排序集合时(通过 Comparator)
4.需要利用导航方法(如 floor(), ceiling(), higher(), lower())进行查找

你可能感兴趣的:(集合框架之Set,java,开发语言)