Java Set 集合:HashSet、LinkedHashSet 与 TreeSet 核心解析

Java Set 集合:HashSet、LinkedHashSet 与 TreeSet 核心解析

Set是一类元素无序(不保证顺序),不可重复、不可索引的集合,Set接口继承与Collection接口。
注:某些Set的实现类是有序,比如LinkedHashSet。

Collection有List和Set两种类型的集合,分别有不同的特点,在开发过程中根据实际应用场景,选择对应类型的集合进行使用。

  1. List:有序、可重复、可索引的集合,有基于数组数据结构的实现类ArrayList和基于双向链表的实现类LinkedList。如果集合需要存储可以重复的元素,必须选择List。在此基础上,根据集合的索引查询频率、插入和删除元素频率等场景实际情况,评估选择ArryList或者LinkedList。
  2. Set:无序、不可重复、不可索引的集合,有基于哈希表+链表+红黑树数据结构的实现类HashSet、基于HashSet+顺序指向链表的实现类LinkedHashSet、基于红黑树数据结构的实现类TreeSet。如果集合需要元素去重,则使用Set。在此基础上,根据是否要求保留数据插入顺序、数据是否有序排列、数据增删查改效率,选择使用HashSet、LinkedHashSet或者TreeSet。

HashSet

HashSet在JDK8之前采用可扩容数组+链表的数据结构进行实现,在JDK8之后,采用可扩容数组+链表+红黑树的数据结构进行实现。

HashSet是基于哈希表的逻辑进行存储数据,因此依赖于hashCode()方法。去重判断则依赖于equals()方法。因此,HashSet存储自定义对象,需要在类中重写hashCode()和equals()方法。这是因为Object类中的equals()方法默认使用对象内存地址进行判断,也就是说只有同一个对象才返回true,而一般情况是两个相同类的对象的属性值相同即表示“相等”。而Object类中的hashCode()是基于内存地址进行计算,同样需要修改为基于属性值进行计算。IDEA提供一键生成自定义类的重写hashCode()和equals()方法的操作。

HashSet对象的元素插入流程如下:

  1. 调用hashCode()方法,计算元素对象的哈希值。
  2. 用对象的哈希值,和HashSet对象的存储元素的数组长度,计算三数组存储的索引。
  3. 判断数组索引中是否有元素(是否指向null),如果没有,则存储元素(指向该元素),完成插入操作。
  4. 若该数组索引中有元素,则遍历链表(可能是链表、也可能是红黑树),调用equals()方法与已存储的元素金进行逐一比较。若有相同的,则表示元素重复,停止插入操作。若没有重复的,则插入到链表尾部。

LinkedHashSet

LinkedHashSet是继承于HashSet的实现类,与HashSet区别在于,LinkedHashSet是有序的。

相比于HashSet,LinkedHashSet中存储元素的Node节点,额外增加了两个引用,一个是指向上一个存储元素的Node节点,另外一个指向下一个存储元素的Node节点,也就是形成了串起插入顺序的双向链表。

用一个例子对比,LinkedHashSet有序,而HashSet无序:

publicclassApp{publicstaticvoidmain(String[] args){// 创建3个Student对象 Student s1 =newStudent("小王",18);Student s2 =newStudent("小李",19);Student s3 =newStudent("小张",20);// 创建一个HashSet,按顺序逐一添加3个Student对象:小王、小李、小张 HashSet<Student> hs =newHashSet<>(); hs.add(s1); hs.add(s2); hs.add(s3);for(Student s : hs){System.out.println(s);}// 迭代打印顺序,与添加顺序不同System.out.println("------------------------------------------------");// 创建一个LinkedHashSet,按顺序逐一添加3个Student对象:小王、小李、小张 LinkedHashSet<Student> lhs =newLinkedHashSet<>(); lhs.add(s1); lhs.add(s2); lhs.add(s3);for(Student s : lhs){System.out.println(s);}// 迭代打印顺序,与添加顺序相同}}
Student{name='小王', age=18} Student{name='小张', age=20} Student{name='小李', age=19} ------------------------------------------------ Student{name='小王', age=18} Student{name='小李', age=19} Student{name='小张', age=20} 进程已结束,退出代码为 0

TreeSet

TreeSet是底层用红黑树作为数据结构的Set实现类。众所周知,红黑树是二叉搜索平衡树,因此TreeSet的增删查改的效率都很高,并且TreeSet存储的元素是有序的。

树这一数据结构要求存储的元素需要能够比较,因此TreeSet存储的对象也需要支持比较。

在Java中,自定义类要支持比较,需要实现一个Comparable的接口,并重写compareTo方法。

publicclassApp2{publicstaticvoidmain(String[] args){// 创建5个Student对象 Student s1 =newStudent("xiaoWang",18);Student s2 =newStudent("xiaoaLi",18);// 与上一个对象年龄相同,名字不同 Student s3 =newStudent("xiaoaLi",20);// 与上一个对象年龄不同,名字相同 Student s4 =newStudent("xiaoaLi",20);// 与上一个对象年龄相同,名字相同 Student s5 =newStudent("liHua",17);// 创建一个TreeSet,按顺序逐一添加5个Student对象 TreeSet<Student> ts =newTreeSet<>(); ts.add(s1); ts.add(s2); ts.add(s3); ts.add(s4);// 添加重复对象  ts.add(s5);for(Student s : ts){// 遍历集合,打印出的对象按照compareTo()方法定义的比较规则排序 System.out.println(s);}}}

终端结果,TreeSet遍历打印出的结果,是按照按照CpomareTo()方法定义的比较规则排序。

Student{name='liHua', age=17}Student{name='xiaoWang', age=18}Student{name='xiaoaLi', age=18}Student{name='xiaoaLi', age=20} 进程已结束,退出代码为 0

存储的Student类实现Comparable接口,重写compareTo方法定义对象之间比较的规则。

publicclassStudentimplementsComparable<Student>{// 其他代码// ...@OverridepublicintcompareTo(Student o){/* 比较规则:若年龄不相同,则年龄大的更大 若年龄相同,则名字字符串大的更大 否则(年龄和名字相同),则两者相等 */if(this.age != o.age){returnthis.age - o.age;}else{returnthis.name.compareTo(o.name);}}}

有些情况,我们需要使用第三方的类(比如String类)进行比较,我们没办法修改第三方类让它实现Comparable接口,重写compareTo方法。

因此,Java还提供了一种在创建 TreeSet 时,传入一个 Comparator 比较器对象的方法。比如:

// 使用Comparator按姓名长度排序TreeSet<Student> ts =newTreeSet<>(newComparator<Student>(){@Overridepublicintcompare(Student s1,Student s2){return s1.getName().length()- s2.getName().length();}});

Read more

优选算法——二分查找

👇作者其它专栏 《数据结构与算法》《算法》《C++起始之路》 二分查找相关题解 1.二分查找 算法思路: a.定义left,right指针,分别指向数组的左右区间。 b.找到待查找区间的中间点mid,找到后分三种情况讨论:         i.arr[mid]==target说明正好找到,返回mid的值;         ii.arr[mid]>target说明[mid,right]这段区间都是大于target的,因此舍去右边区间,在左边[left,mid-1]的区间继续查找,即让right=mid-1,然后重复b过程;         iii.arr[mid]<target说明[left,mid]这段区间的值都是小于target的,因此舍去左边区间,在右边区间[mid+1,right]

By Ne0inhk
深入理解 C++ 哈希:从概念到实战应用

深入理解 C++ 哈希:从概念到实战应用

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 前言: 一、哈希的概念 1.1 直接定址法 1.2 哈希冲突 1.3 负载因子 1.4 将关键字转为整数 二、哈希函数 2.1 除法散列法 / 除留余数法 2.2 乘法散列法(了解) 2.3 全域散列法(了解)  2.4 其他方法(了解)  三、处理哈希冲突(

By Ne0inhk
【优选算法必刷100题】第031~32题(前缀和算法):连续数组、矩阵区域和

【优选算法必刷100题】第031~32题(前缀和算法):连续数组、矩阵区域和

🔥艾莉丝努力练剑:个人主页 ❄专栏传送门:《C语言》、《数据结构与算法》、C/C++干货分享&学习过程记录、Linux操作系统编程详解、笔试/面试常见算法:从基础到进阶 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬艾莉丝的简介: 🎬艾莉丝的算法专栏简介: 目录 031  连续数组 1.1  解法一:暴力解法 1.2  解法二:前缀和在哈希表中 1.3  算法实现 1.3.1  C++实现 1.3.2  Java实现 1.4  博主手记 032  矩阵区域和 2.1

By Ne0inhk
贪心算法篇——万千抉择中的唯一考量,最优解追寻的跬步累积(1)

贪心算法篇——万千抉择中的唯一考量,最优解追寻的跬步累积(1)

文章目录 * 引言:在选择的海洋中 * 贪心算法的哲学:局部最优,全球最优 * 贪心算法的经典应用 * 贪心算法的局限与挑战 * 结语:智者的选择,最优的未来 引言:在选择的海洋中 在人生的旅途上,每个人都要面临无数的选择。每一个选择,都是一次抉择;每一次抉择,都是命运的交汇点。数学与计算机科学的世界里,贪心算法正是对这种“选择”的一种深刻体现。在一系列的选择面前,贪心算法如同一位睿智的旅行者,始终秉持着最优的哲学:每一次决策都应基于局部最优,以期在最后抵达全局最优的境地。 贪心算法(Greedy Algorithm),正如其名所示,是一种每次都选择当前看起来最优解的算法。这种算法策略简单却充满智慧,常常能够解决很多看似复杂的问题。它通过一种局部的、贪婪的方式,一步步走向最终解。然而,正如智慧的旅行者需要对道路有所预见一样,贪心算法也有其适用的范围,只有在满足某些条件时,它才能发挥出最优解的魅力。 在这篇报告中,我们将深入探讨贪心算法的基本理念、适用范围、经典应用,并通过具体的代码示例,揭开这一算法的神秘面纱。 贪心算法的哲学:

By Ne0inhk