Set详解

今日金句

        日拱一卒 功不唐捐


在上一篇博客中,我们详细介绍了Java集合框架中的List集合,今天我们来详细介绍它的兄弟——Set集合,让我们开始今天的欢乐学习之旅吧

目录

今日金句

        日拱一卒 功不唐捐

一、set概述

1.1概念

1.2体系

1.3特点

1.3.1 无序性

1.3.2 唯一性

1.4 通用方法 

二、遍历方式

2.1foreach

2.2迭代器

三、HashSet

3.1 HashSet概述

3.1.1概念

3.1.2数据结构

3.2HashSet去重原理

3.2.1字符串去重

3.2.2对象去重

3.2.3总结

四、TreeSet

4.1概述

4.1概念

4.2数据结构

4.2TreeSet的特点

4.3自然排序

4.4自定义排序


一、set概述

1.1概念

Set是Java中的一个集合类型,它继承自Collection接口,表示一组不包含重复元素的无序集合,本质上是一个容器。

1.2体系

Set集合由HashSet、TreeSet、LinkedHashSet组成。

1.3特点

从set集合的概念中我们就能看出其两个特点:

1.3.1 无序性

Set集合中的元素没有顺序之分,即元素无法通过索引访问

1.3.2 唯一性

Set集合中的元素不可重复,即无法有两个相同的元素存在于同一个Set集合中

1.4 通用方法 

方法说明

add(E e)

将指定元素添加到集合中(如果已经存在则不添加,返回false)
remove(Object o)从集合中移除指定元素(如果元素不存在则不进行操作,返回false)
contains(Object o)判断集合中是否包含指定元素,即判断指定元素是否是集合中的一个元素
isEmpty() 判断集合是否为空

size()

返回集合中元素的个数

clear() 从集合中移除所有元素

二、遍历方式

对比List集合的三种遍历方式,我们再根据Set集合的特点——无序性,可以得出Set集合只有两种遍历方式,缺少for循环根据下标遍历

2.1foreach

	public static void main(String[] args) {
		Set s = new TreeSet();
		s.add(1);
		s.add(2);
		s.add(3);
		for (Object object : s) {
			System.out.println(object);// 1 2 3
		}
	}

2.2迭代器

	public static void main(String[] args) {
		Set s = new TreeSet();
		s.add(1);
		s.add(2);
		s.add(3);
		Iterator iterator = s.iterator();
		while (iterator.hasNext()) {
			System.out.println(iterator.next());// 1 2 3
		}
	}

三、HashSet

3.1 HashSet概述

3.1.1概念

HashSet是Java中的一个类,实现了Set接口,底层使用哈希表(来存储元素,是Java集合框架中常用的一种无序、不可重复的集合类型。

3.1.2数据结构

HashSet的数据结构是哈希表

哈希表(hash table,也称散列表)是一种根据关键码值(key value)进行直接访问的数据结构。哈希表通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。换句话说,哈希表中的每一个元素都是通过哈希算法计算出来的一个索引,通过该索引可以快速在表中查找到相应的元素

哈希表的实现方法主要包括开放寻址法链表法两种。在开放寻址法中,哈希表会将发生冲突的元素放入不同的槽位里,因此需要对哈希表进行线性、二次探测等操作,期望时间复杂度为O(1),均摊时间复杂度为O(1)。而链表法则是在哈希表上使用单向链表或者双向链表来解决哈希碰撞的问题,产生碰撞的元素会被放入同一个链表中,以保证时间复杂度尽量地接近理想状态。

哈希表底层实现常用的数据结构是数组。虽然哈希表底层是通常数组,但其动态扩容、迁移元素等操作的实现,使得哈希表具有比数组更加灵活和高效的特点。

3.2HashSet去重原理

3.2.1字符串去重

        HashSet s = new HashSet();
		s.add("a");
		s.add("b");
		s.add("c");
		System.out.println(s);//[a, b, c]
		s.add("b");
		System.out.println(s);//[a, b, c]

因为HashSet的特点之一就是唯一性,所以“b”没有新增成功,那么原理是什么呢?

当我们将多个字符串添加到HashSet集合中时,HashSet会根据字符串的哈希值来进行判断,如果哈希值相同并且两个字符串相等(使用String类的equals()方法进行比较,即比较内存地址),则认为这是同一个字符串,只会保留一个。

3.2.2对象去重

	public static void main(String[] args) {
		HashSet s = new HashSet();
		s.add(new Student("zs", 16));
		s.add(new Student("ls", 17));
		s.add(new Student("ww", 18));
		System.out.println(s);//[Student{name='zs', age=16}, Student{name='ww', age=18}, Student{name='ls', age=17}]
		s.add(new Student("ww", 18));
		System.out.println(s);//[Student{name='zs', age=16}, Student{name='ww', age=18}, Student{name='ww', age=18}, Student{name='ls', age=17}]

	}

那为什么new Student("ww", 18)新增成功呢?

是的,与上篇博客ArrayList集合对象去重类似,当学习的事物有共性时,你要有举一反三的能力,这样你会感到一种通透,就像打通任督二脉一般,没错,这是因为实例化了,使得第二个 Student("ww", 18)新开辟了一个内存地址,这让两个对象的hash值也不同,导致新增成功!

怎么解决呢,达到去重的效果?

没错,看过上篇博客的你又能把答案脱口而出,重写equals方法!什么是学习的快乐?这就是!!拥有了举一反三的能力是在学习时是无比快乐的,话不多说,让我们快点开始操作吧,上代码!

package com.xqx.set;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

public class demo1 {
	public static void main(String[] args) {
		HashSet s = new HashSet();
		s.add(new Student("zs", 16));
		s.add(new Student("ls", 17));
		s.add(new Student("ww", 18));
		System.out.println(s);//[Student{name='zs', age=16}, Student{name='ww', age=18}, Student{name='ls', age=17}]
		s.add(new Student("ww", 18));
		System.out.println(s);//[Student{name='zs', age=16}, Student{name='ww', age=18}, Student{name='ww', age=18}, Student{name='ls', age=17}]

	}
}

class Student {
	private String name;
	private int age;

	public Student(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
//
	@Override
	public boolean equals(Object o) {
		System.out.println("调用了我心爱的equals方法");
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		Student student = (Student) o;
		return age == student.age && name.equals(student.name);
	}
	@Override
	public String toString() {
		return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
	}
}

可是,为什么重写equals方法后,它还是没有去重,甚至都没打印“调用了我心爱的equals方法”?没事,不要伤心不要难过,这就像同一种数学题型,解题总的思路是一致的,但在某一步骤可能需要变通一下。让我们来回顾字符串去重的原理,“当我们将多个字符串添加到HashSet集合中时,HashSet会根据字符串的哈希值来进行判断”,看到哈希值是否给你提供了解题灵感?

没错!聪明的你已经想出来了,就是重写HashCode方法!!!

package com.xqx.set;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

public class demo1 {
	public static void main(String[] args) {
		HashSet s = new HashSet();
		s.add(new Student("zs", 16));
		s.add(new Student("ls", 17));
		s.add(new Student("ww", 18));
		System.out.println(s);//[Student{name='ww', age=18}, Student{name='zs', age=16}, Student{name='ls', age=17}]
		s.add(new Student("ww", 18));
		System.out.println(s);//[Student{name='ww', age=18}, Student{name='zs', age=16}, Student{name='ls', age=17}]

	}
}

class Student {
	private String name;
	private int age;

	public Student(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
//
	@Override
	public boolean equals(Object o) {
		System.out.println("调用我心爱equals方法。。。");
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		Student student = (Student) o;
		return age == student.age && name.equals(student.name);
	}
	@Override
	public String toString() {
		return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
	}


	@Override
	public int hashCode() {
		System.out.println("调用我最爱的hashCode方法");
		return Objects.hash(name, age);
	}

}

打印结果:

OK,到这里已经实现了对象去重,但我知道你小小的脑袋现在是充满了大大的疑惑的。

没事,哥会为你解答

问题1:为什么重写equals方法后,它没有去重,甚至都没打印“调用了我心爱的equals方法”?

答:虽然重写了equals方法,但是没有重写hashCode方法。在判断元素是否相同时,HashSet首先会根据被添加元素的哈希值来确定它应该插入集合的位置。因为Student类没有重写hashCode方法,所以HashSet会默认使用Object类的hashCode方法,根据对象的内存地址计算哈希值。在new Student("ww", 18)被添加到集合中时,它的哈希值与集合中已有元素的哈希值不同,因此会被添加到集合中,而哈希值不同,根本就不会调用equals方法,所以不会打印。

问题2:为什么重写hashCode方法后,每新增一次就会调用一次该方法?

答:由于每个Student对象的哈希值都是根据其属性值计算得到的,并且对象属性值不同,所以每次新增一个新的Student对象都会调用一次更新后的hashCode方法,根据对象的属性值生成哈希值,如果已经有相同哈希值的元素了,则使用equals方法判断是否真的相等,如果是则不进行插入,否则仍然会插入,这就是为什么每新增一次就会调用一次的原因。

3.2.3总结

无论是字符串去重,还是对象去重,HashSet都会比较Hash值,再去比较内存地址,只有这两者都相同才会去重。

  • 如果是字符串去重,HashSet会默认实现
  • 如果是对象去重,需要确保对象重写了hashCode()方法和equals()方法

四、TreeSet

4.1概述

4.1概念

TreeSet 是一个基于红黑树(Red-Black Tree)实现的有序集合类,实现了set接口,在集合中的元素按照一定的顺序排列。

4.2数据结构

TreeSet 的数据结构是红黑树(以下内容不太了解)

红黑树是一种自平衡二叉搜索树,它能够保证在最坏情况下基本动态集合操作的时间复杂度为 O(log n),非常适合用于数据访问较为频繁、插入和删除操作不是特别频繁的场景。

红黑树在结构上是由满足以下五个条件的二叉搜索树组成的:

  1. 树中的每个节点都是黑色或红色。
  2. 根节点是黑色的。
  3. 如果一个节点为红色,则它的子节点必须是黑色的。
  4. 每个叶子节点(NIL节点,不包含键值的节点)都是黑色的。
  5. 从任意一个节点到叶子节点的每个路径上,所包含的黑色节点数目相同。

红黑树是一种自平衡的二叉搜索树,这意味着插入或删除节点时,红黑树会通过旋转和变色操作来保持满足上面的五个条件。通过自平衡,红黑树能够很快地返回排序结果或完成查找、插入、删除等操作。

红黑树的优点包括:

  • 能够在 O(log n) 的时间复杂度内完成基本的动态集合操作,包括插入、删除、查找等;
  • 在树形结构上作了特殊限制,避免了二叉搜索树可能退化成线性链的情况;
  • 具有平衡性质,自平衡的速度较快,使得相应的算法更具优势。

平衡二叉搜索树(Balanced Binary Search Tree,简称 BBST)是一种基于二叉树的数据结构,在二叉搜索树的基础上,通过一些自平衡策略,使得每个节点的左右子树高度相差不超过1,从而保证在最坏情况下基本动态集合操作的时间复杂度为 O(log n)。

常见的 BBST 包括 AVL 树、红黑树、B-树等。其中红黑树是一种常用的 BBST,能够在插入、删除元素时自动地进行节点的旋转和颜色的变换,以保证树的平衡。其他的 BBST,例如 AVL 树和 Treap 等,也具有类似的自平衡机制。

BBST 的特点是:

  • 数据按照一定的顺序存储,支持快速的搜索、插入和删除操作;
  • 树的高度对数据的总运行时间有重要影响;
  • 通过高效的自平衡机制能够保证树的高度始终在 O(log n) 级别。

二叉树是一种常见的树形数据结构,它由一组节点和一组有向边组成,每个节点最多有两个子节点。特别的,二叉树中的每个节点都可以称为是它之下节点的父节点,而它之上的节点则是它的祖先节点。同时,一个节点的子节点被称为是它的后代节点。

二叉树的特点是:

  • 每个节点最多有两个子节点,称为左子节点和右子节点;
  • 左子节点的值小于父节点,右子节点的值大于父节点;
  • 左右子树都是二叉树,它们也都满足二叉搜索树的性质;
  • 每个节点只有一个父节点,除了根节点没有父节点以外。

4.2TreeSet的特点

  • 必须实现 Comparable 接口或者传入一个比较器( Comparator )。
  • 内部基于红黑树实现,保证元素有序。
    • 这里的“有序”与List集合的有序性不同,List 的有序性是指元素的顺序和插入顺序一致,而TreeSet 的有序是指按照某种规则的排序
  • 不允许添加重复元素。
  • 支持根据元素顺序进行查找、遍历、子集操作等。

4.3自然排序

TreeSet 底层使用红黑树来存储元素,并使用元素的 compareTo() 方法进行比较。当向 TreeSet 中添加元素时,先将元素插入到红黑树的合适位置上,并标记节点为红色(因为红黑树中新增节点的默认颜色为红色)。随着元素的不断添加,红黑树可能会出现不平衡的情况,这时候就需要使用红黑树的自平衡机制进行调整。具体来说,红黑树使用节点颜色和旋转操作来保证树的平衡。

不懂吧,其实我也不懂,对红黑树不了解,等以后学习了数据结构再和大家来分享。

在这里,只要明白对于自然排序而言,会使用compareTo()方法进行比较。

package com.xqx.set;

import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

public class demo1 {
	public static void main(String[] args) {
		TreeSet s = new TreeSet();
		s.add(new Student("zs", 16));
		s.add(new Student("ww", 18));
        s.add(new Student("ls", 17));
		for (Object object : s) {
			System.out.println(object);
		}
	}
}

class Student implements Comparable {
	private String name;
	private int age;

	public Student(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	@Override
	public boolean equals(Object o) {
		System.out.println("调用我心爱equals方法。。。");
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		Student student = (Student) o;
		return age == student.age && name.equals(student.name);
	}
	@Override
	public String toString() {
		return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
	}


	@Override
	public int hashCode() {
		System.out.println("调用我最爱的hashCode方法");
		return Objects.hash(name, age);
	}


	@Override
	public int compareTo(Object o) {
		Student student = (Student) o;
		System.out.println(this.age + "aaa");
		return this.age - student.age;
	}
}

打印结果:

Student{name='zs', age=16}
Student{name='ls', age=17}
Student{name='ww', age=18}

4.4自定义排序

TreeSet 中的元素进行自定义排序,需要提供一个 Comparator 对象,也就是比较器,该对象实现了 Comparator 接口,并覆盖了 compare() 方法。compare() 方法可以实现自定义的比较逻辑,从而完成自定义排序。

package com.xqx.set;

import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

public class demo3 {
	public static void main(String[] args) {
		TreeSet ts = new TreeSet();
		ts.add(new Student(1, "zs", 16));
		ts.add(new Student(2, "ls", 17));
		ts.add(new Student(3, "es", 16));
		ts.add(new Student(4, "ww", 18));
		System.out.println("=============默认id排序===============");
		for (Object object : ts) {
			System.out.println(object);
		}
		TreeSet tsPlus = new TreeSet(new Comparator<Student>() {

			@Override
			public int compare(Student s1, Student s2) {
				if (s1.getAge() - s2.getAge() == 0)
					return s1.getName().compareTo(s2.getName());
				return s1.getAge() - s2.getAge();
			}

		});
		for (Object object : ts) {
			tsPlus.add(object);
		}
		System.out.println("=============默认年龄排序,若年龄一致,按名字排序===============");
		for (Object object : tsPlus) {
			System.out.println(object);
		}

	}
}

class Student implements Comparable<Student> {
	private int id;
	private String name;
	private int age;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public Student(int id, String name, int age) {
		this.id = id;
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	@Override
	public int hashCode() {
		System.out.println("调用我心爱hashCode方法。。。");
		final int prime = 31;
		int result = 1;
		result = prime * result + age;
		result = prime * result + id;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		System.out.println("调用我心爱equals方法。。。");
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Student other = (Student) obj;
		if (age != other.age)
			return false;
		if (id != other.id)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "Student [id=" + id + ", name=" + name + ", age=" + age + "]";
	}

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

}

打印结果:

=============默认id排序===============
Student [id=1, name=zs, age=16]
Student [id=2, name=ls, age=17]
Student [id=3, name=es, age=16]
Student [id=4, name=ww, age=18]
=============默认年龄排序,若年龄一致,按名字排序===============
Student [id=3, name=es, age=16]
Student [id=1, name=zs, age=16]
Student [id=2, name=ls, age=17]
Student [id=4, name=ww, age=18]

好啦,今天的分享就到此为止!希望你看完本篇文章有所收获,祝你变得更强!!!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值