【Java】TreeSet

一、介绍

TreeSet 也是 Set集合体系 中的一员,在 TreeSet 中,也没有新的方法需要学习,直接使用上面 Colleciton 里面的方法就行了。

image-20240427202209784

TreeSet 中延续了 Set集合 的两个特点:不重复、无索引,但是它是可以给元素进行排序的。

可排序:默认情况下,是按照从小到大的顺序进行排列的,因为在底层,TreeSet 是基于红黑树的数据结构实现排序的,增删改查性能都比较好。


二、练习:TreeSet 对象排序

需求:存储整数并进行排序

首先来创建 TreeSet集合 的对象,注意泛型不能直接写 int,而是需要写对应的包装类。

TreeSet<Integer> ts = new TreeSet<>();

然后添加元素,并打印 ts

//2.添加元素
ts.add(4);
ts.add(5);
ts.add(1);
ts.add(3);
ts.add(2);

程序运行完毕,可以发现已经排序成功,并且默认排序规则:从小到大

//3.打印集合
System.out.println(ts); // [1, 2, 3, 4, 5]

同样遍历也是可以的。

由于 TreeSetSet体系 中的一员,因此三种遍历方式它都是可以的。

//迭代器
Iterator<Integer> it = ts.iterator();
while(it.hasNext()){
    int i = it.next();
    System.out.println(i);
}

System.out.println("--------------------------");

//增强for
for (int t : ts) {
    System.out.println(t);
}

这里的 forEach() 在底层是通过 增强for 来进行遍历的。

this 表示的是当前方法的调用者,而我们方法的调用者就是 ts,即集合

image-20240427180608528
//lambda
ts.forEach( i-> System.out.println(i));

三、字符串比较规则

  • 对于数值类型:IntegerDouble,默认按照从小到大的顺序进行排序。
  • 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序。

很多课程中都会这么去解释:字符和字符串默认的排序是按照字典的顺序进行排列的。

这句话表示的结果是对的,因为最终的排序结果跟字典里面的是一样的。

但是如果细细琢磨源码的话,这句话就不对了,因为在源码中,它是按照 ASCII码表 的数字来进行排列的,只不过刚好 ASCII码表 中字母的顺序跟字典上的顺序是一样的。

因此在以后,建议大家还是按照标准的方式来进行理解:按照字符在ASCII码表中的数字升序进行排序。

那如果字符串里面的内容比较多,那它是怎么排列的呢?


如果字符串里的字符比较多,那么它就是从首字母开始,挨个比较的,要注意的是,此时跟字符串的长度是没有什么关系的。

首先来看 "aaa""ab",在比较的时候,首先比第一个字母,发现第一个字母都是 a

继续往后,来比第二个字母,第二个字母就不一样了,这个时候就已经能确定大小关系了,'a'b 大,此时后面的就不会再看了。

因此 "aaa" 要在 "ab" 的前面。

image-20240427191945244

接下来看 "ab""aba"

按照刚刚的规则,先比较第一个字母,发现是一样的,继续比,直到比到第三个字母,"ab" 没有第三个字母,但是 "aba" 有第三个字母,因此它就会认为 "aba" 要更大,因此 "aba" 排在后面。

image-20240427192314007

接下来看 "aba""cd",此时第一个字母就能确定大小关系了,后面就不会再去看了,因此 "aba" 要排在前面。

image-20240427193122882

四、自定义对象比较规则

1)引出问题

需求:创建TreeSet集合,并添加3个学生对象
学生对象属性:
    姓名,年龄。
    要求按照学生的年龄进行排序
    同年龄按照姓名字母排列(暂不考虑中文)
    同姓名,同年龄认为是同一个人

首先得写一个学生类

public class Student {
    //姓名
    private String name;
    //年龄
    private int age;

    // 无参构造、有参构造、get、set方法
}

首先来创建三个学生对象

PS:我们在起名字的时候暂时不用中文,因为中文在码表中对应的数字我们不知道。

Student s1 = new Student("zhangsan",23);
Student s2 = new Student("lisi",24);
Student s3 = new Student("wangwu",25);

接下来就创建 TreeSet集合 对象

 TreeSet<Student> ts = new TreeSet<>();

添加元素

ts.add(s3);
ts.add(s2);
ts.add(s1);
ts.add(s4);

打印集合看看效果

System.out.println(ts);

程序运行完毕发现,我们没有看见想要的结果,而是一个报错:main方法33行 报错。

image-20240427195215516

报错原因其实也很简单,刚刚的 Student 是我们自己写的,我们并没有给它去添加一个默认的比较规则,此时 TreeSet 就比知道如何比较,因此在添加元素的时候就报错了。

那我们如何添加默认的比较规则呢?


2)TreeSet 的两种比较方式

方式一:默认排序,它也叫做 自然排序,让JavaBean类实现 Comparable接口 指定比较规则。

方式二:比较器排序,创建 TreeSet对象 的时候,传递比较器 Comparator,然后再去指定比较的规则

使用原则:默认使用第一种,若干第一种不能满足当前需求,就使用第二种。

例如字符串里面的排序规则Java已经定义好了,它默认是从首字母开始,按照ASCII码表的值来进行排列的。

但如果我不想要按照这个规则排序,我想要按照长度排序,你能去修改字符串里面的源码吗?不能吧,或者是非常的麻烦,几乎是不可能实现的,此时我们就可以使用第二种方式来进行排序。

方式一和方式二如果同时存在,以方式二为准。

例如String中Java已经定义好了默认的排序规则,此时我再给它定义一个比较器,这种情况下两种方式都存在,我们实际运行代码的时候,就是以方式二比较器的规则为准。


3)方式一:默认的排序规则 / 自然排序

Student类 实现 Comparable接口,重写里面的抽象方法,再指定比较规则。

注意,这个接口前面是有泛型的

image-20240427201553674

由于类型确定,因此泛型的地方直接写 Student 就好,就不需要延续泛型了。

表示我们要比较排序的就是 Student,然后重写里面的抽象方法。

image-20240427201645445

此时就可以在 compareTo() 中指定排序的规则。

需求:只看年龄,我想要按照年龄的升序进行排列

@Override
public int compareTo(Student o) {
    return this.getAge() - o.getAge();
}

此时再去运行我们刚刚写的测试类,可以发现它在打印的时候就已经按照年龄的升序进行排序了。

image-20240427201935339

此时有同学心里肯定有两个小疑问?

此时我们还需要去重写 hashCode()equals() 呢?

不需要,在之前我们讲解的 hashCode()equals() 跟哈希表有关的,而现在我们讲解的是 TreeSet集合TreeSet集合 底层不是哈希表,而是红黑树。

因此当我们把 Student 添加到 TreeSet集合 中的时候, Student 中不需要去重写 hashCode()equals() ,但是我们需要去指定它的排序规则。


4)理解规则

建议结合视频观看:集合进阶-14-TreeSet第一种排序方式超详细讲解_哔哩哔哩_bilibili,定位到 21:47

在添加元素的时候,它会反复不断去调用 compareTo() 直到确认自己的位置

image-20240427203705148

我们在 compareTo() 中打印 thiso ,就能很明确的感受到 thiso 到底指的是是什么了

@Override
//this:表示当前要添加的元素
//o:表示已经在红黑树存在的元素

//返回值:
//负数:表示当前要添加的元素是小的,存左边
//正数:表示当前要添加的元素是大的,存右边
//0 :表示当前要添加的元素已经存在,舍弃
public int compareTo(Student o) {
    System.out.println("--------------");
    System.out.println("this:" + this);
    System.out.println("o:" + o);
    //指定排序的规则
    //只看年龄,我想要按照年龄的升序进行排列
    return this.getAge() - o.getAge();
}

第一次插入 wangwu、25 的时候,一开始树中是没有节点的,因此第一次是 wangwu、25 自己跟自己比较了一下,将它当做根节点。

image-20240427204242525

我们真正要看的是从第二次开始,第二次添加的是 s2,也就是 lisi、24,因此 this 就是 lisi、24

它需要跟根节点 wangwu、25 比较,因此 o 就是 wangwu、25,比完后用 24 - 25 发现得到一个负数,所以 lisi、24 就要存到左边去。

其他的同理。

image-20240427204644553

5)方式二:比较器排序

需求:请自行选择比较器排序和自然排序两种方式;
要求:存入四个字符串, “c”, “ab”, “df”, “qwer”
按照长度排序,如果一样长则按照首字母排序

第一步创建集合

TreeSet<String> ts = new TreeSet<>();

添加元素

ts.add("c");
ts.add("ab");
ts.add("df");
ts.add("qwer");

打印集合,如果我什么操作都没做的话,它就是采取默认的方式进行排序的:从首字母开始比较,跟长度无关。

System.out.println(ts);

那为什么是这样的呢?其实这个代码就是 String 里面已经写好的代码。

选中 String ctrl + b,它在源码中也实现了 Comparable接口

image-20240427210126494

并且它也重写了 compareTo(),在这个方法里面就是字符串默认的排序规则。

image-20240427210457762

但由于现在这个规则不满足我们的要求了,又不能去修改源码,此时就可以采取第二种排序方式:比较器排序。

即我们在创建集合对象的时候,传递一个比较器的对象就行了。

那这个比较器叫什么名字呢?打开 API帮助文档 查看,找到 TreeSet 的构造方法,之前我们采取的是空参构造,空参构造采用的就是默认的比较规则,但是现在默认的比较规则不满足我的要求了,此时我们就可以用下面的第三个构造。

image-20240427211158880

在第三个构造中,需要传入 Comparator,这个 Comparator 其实就是一个比较器,但是它也是以接口的形式来体现的,因此我们真正传的还是这个接口的实现类对象。

image-20240427212022202
//o1:表示当前要添加的元素
//o2:表示已经在红黑树存在的元素
//返回值规则跟之前是一样的
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        // 按照长度排序
        int i = o1.length() - o2.length();
        //如果一样长则按照首字母排序
        i = i == 0 ? o1.compareTo(o2) : i;
        return i;
    }
});

重新运行程序,可以发现运行结果已经是我们想要的了

System.out.println(ts); // [c, ab, df, qwer]

那这里能不能改成 Lambda呢?改成 Lambda有个前提,那就是这里的接口必须为函数式接口。

选中 Comparator ctrl + b,可以发现这个接口就是一个函数式接口,因此我们可以改成Lambda表达式。

image-20240427212738139
TreeSet<String> ts = new TreeSet<>((o1, o2)->{
    // 按照长度排序
    int i = o1.length() - o2.length();
    //如果一样长则按照首字母排序
    i = i == 0 ? o1.compareTo(o2) : i;
    return i;
});

五、练习

需求:创建5个学生对象
属性:(姓名,年龄,语文成绩,数学成绩,英语成绩),
按照总分从高到低输出到控制台
如果总分一样,按照语文成绩排
如果语文一样,按照数学成绩排
如果数学成绩一样,按照英语成绩排
如果英文成绩一样,按照年龄排
如果年龄一样,按照姓名的字母顺序排
如果都一样,认为是同一个学生,不存。
第一种:默认排序/自然排序
第二种:比较器排序
默认情况下,用第一种排序方式,如果第一种不能满足当前的需求,采取第二种方式。
课堂练习:
    要求:在遍历集合的时候,我想看到总分。

Student.java

第一种:默认排序/自然排序

第二种:比较器排序

默认情况下,用第一种排序方式,如果第一种不能满足当前的需求,采取第二种方式。

public class Student2 implements Comparable<Student2> {
    //姓名
    private String name;
    //年龄
    private int age;
    //语文成绩
    private int chinese;
    //数学成绩
    private int math;
    //英语成绩
    private int english;
    
    // 无参构造、有参构造、get、set方法

    public String toString() {
        return "Student2{name = " + name + ", age = " + age + ", chinese = " + chinese + ", math = " + math + ", english = " + english + "}";
    }

   /* 按照总分从高到低输出到控制台
    如果总分一样,按照语文成绩排
    如果语文一样,按照数学成绩排
    如果数学成绩一样,按照英语成绩排
    如果英文成绩一样,按照年龄排
    如果年龄一样,按照姓名的字母顺序排
    如果都一样,认为是同一个学生,不存。*/
    @Override
    public int compareTo(Student2 o) {
        int sum1 = this.getChinese() + this.getMath() + this.getEnglish();
        int sum2 = o.getChinese() + o.getMath() + o.getEnglish();

        //比较两者的总分
        int i = sum1 - sum2;
        //如果总分一样,就按照语文成绩排序
        i = i == 0 ? this.getChinese() - o.getChinese() : i;
        //如果语文成绩一样,就按照数学成绩排序
        i = i == 0 ? this.getMath() - o.getMath() : i;
        //如果数学成绩一样,按照英语成绩排序(可以省略不写,因为如果总分、英文都一样,那么英语肯定也一样)
        i = i == 0 ? this.getEnglish() - o.getEnglish() : i;
        //如果英文成绩一样,按照年龄排序
        i = i == 0 ? this.getAge() - o.getAge() : i;
        //如果年龄一样,按照姓名的字母顺序排序,此时需要调用字符串默认的排序方式
        i = i == 0 ? this.getName().compareTo(o.getName()) : i;
        return i;
    }
}

接下来在测试类中创建学生对象

//1.创建学生对象
Student2 s1 = new Student2("zhangsan",23,90,99,50);
Student2 s2 = new Student2("lisi",24,90,98,50);
Student2 s3 = new Student2("wangwu",25,95,100,30);
Student2 s4 = new Student2("zhaoliu",26,60,99,70);
Student2 s5 = new Student2("qianqi",26,70,80,70);

创建集合的时候,默认情况下会使用 ArrayList 会比较多,因为这个集合最常见。

如果想要数据唯一,就可以在 Set集合 中选择,Set集合 默认情况下可以使用 HashSet

但如果又要唯一,又要排序,就可以使用 TreeSet

//2.创建集合
TreeSet<Student> ts = new TreeSet<>();

//3.添加元素
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);

//4.打印集合
//System.out.println(ts);
for (Student2 t : ts) {
    System.out.println(t);
}

六、总结

1、TreeSet集合 的特点是什么样的?

  • 可排序、不重复、无索引
  • 底层基于红黑树实现排序,增删改查性能较好

2、TreeSet集合 自定义排序规则有几种方式

  • 方式一:默认排序,JavaBean类实现Comparable接口,制定比较规则
  • 方式二:比较器排序,创建 TreeSet集合 时,自定义Comparator比较器,指定比较规则

使用原则:默认使用第一种,若干第一种不能满足当前需求,就使用第二种。

例如字符串里面的排序规则Java已经定义好了,它默认是从首字母开始,按照ASCII码表的值来进行排列的。

但如果我不想要按照这个规则排序,我想要按照长度排序,你能去修改字符串里面的源码吗?不能吧,或者是非常的麻烦,几乎是不可能实现的,此时我们就可以使用第二种方式来进行排序。

方式一和方式二如果同时存在,以方式二为准。

例如String中Java已经定义好了默认的排序规则,此时我再给它定义一个比较器,这种情况下两种方式都存在,我们实际运行代码的时候,就是以方式二比较器的规则为准。

不管是方式一还是方式二,都是跟接口有关的,当我们重写了接口之后,方法的返回值有什么特点?

3、方法返回值的特点

  • 负数:表示当前要添加的元素是小的,存左边
  • 整数:表示当前要添加的元素是大的,存右边
  • 0:表示当前要添加的元素已经存在,舍弃
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值