【Java 数据结构与算法】-Map和Set以及其实现类

文章介绍了Java中Map和Set的概念、模型和常用方法,详细讲解了TreeMap和TreeSet的有序性、可比较性,以及HashMap和HashSet的无序性、覆写规则。还讨论了它们的底层数据结构,如红黑树和哈希桶,并提供了示例代码来展示如何使用这些数据结构。此外,文章还提到了HashMap和HashSet在空间申请、容量调整、树化和解树化等方面的知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者:学Java的冬瓜
博客主页:☀冬瓜的主页🌙
专栏【JavaEE】
分享
主要内容:Map和Set的常用方法以及其特性。TreeMap和TreeSet的性质,HashMap和HashSet的性质。关于HashMap的面试题。

在这里插入图片描述

一、Java中常见集合

在这里插入图片描述

二、Map和Set

1、概念和场景

Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。以前常见的 搜索方式有:

  1. 直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢
  2. 二分查找,时间复杂度为O(logN) ,但搜索前必须要求序列是有序的

上述排序比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如:

  1. 根据姓名查询考试成绩
  2. 通讯录,即根据姓名查询联系方式
  3. 不重复集合,即需要先搜索关键字是否已经在集合中

可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,而Map和Set是 一种适合动态查找的集合容器

2、模型

在这里插入图片描述

3、Map和Set常用方法

  • 注意:Map 没有继承 Iterator 所以不能返回迭代器,而 Set 继承了 Iterator 可以采用迭代器遍历

1>Map常用方法

Map的常用方法:

方法解释
V get(Object key)返回 key 对应的 value
V getOrDefault(Object key, V defaultValue)返回 key 对应的 value,key 不存在,返回默认值
V put(K key, V value)设置 key 对应的 value
V remove(Object key)删除 key 对应的映射关系
Set keySet()返回所有 key 的不重复集合
Collection values()返回所有 value 的可重复集合
Set<Map.Entry<K, V>> entrySet()返回所有的 key-value 映射关系
boolean containsKey(Object key)判断是否包含 key
boolean containsValue(Object value)判断是否包含 value
@ Map方法演示

接下来我们在代码中来使用相关的方法,简单的就不演示了,以下代码只演示了keySet,values,entrySet三个方法。
HashMap和TreeMap都都适用以下方法,因为它们都是是Map,是key-value模型。

代码如下:

public class Main {
    public static void main(String[] args) {
        Map<Integer,String> map1 = new TreeMap<>();
        map1.put(1,"张三");
        map1.put(3,"lisi");
        map1.put(5,"lisi");

        System.out.println("keySet获取map的所有key的不重复集合");
        Set<Integer> keySets = map1.keySet();
        System.out.println(keySets);

        System.out.println("values获取map所有value的可重复集合");
        Collection<String> values = map1.values();
        System.out.println(values);

        System.out.println("entrySet获取所有的key-value映射关系");
        // entrySet方法功能:把map的key和value打包成Map.Entry<Integer,String>,然后放入Set中作为Set的key
        Set<Map.Entry<Integer, String>> entries = map1.entrySet();
        System.out.println("遍历1 toString:");
        System.out.println(entries);
        System.out.println("遍历2 foreach:");
        for (Map.Entry<Integer,String> entry : entries) {
            System.out.print(entry.getKey() + ":" + entry.getValue() + "   ");
        }
    }
}

结果:

values获取map所有value的可重复集合
[张三, lisi, lisi]
entrySet获取所有的key-value映射关系
遍历1 toString:
[1=张三, 3=lisi, 5=lisi]
遍历2 foreach:
1:张三   3:lisi   5:lisi   

在上面代码中,使用Set<Map.Entry<k,v>> entrySet()把Map打包成Map.Entry<k,v>,然后把它放进Set后遍历使用foreach时,使用了以下方法的前两个。

k getKey()             返回ertry中的key
v getValues()          返回entry中的value
v setValue(V value)    将键值对中的value替换为指定value

那么问题来了,不能直接用foreach遍历Map吗?
不行!要想用foreach遍历,包括要用迭代器,必须实现Iterable接口,而Map没有实现,Set实现了。所以把Map变成Set再用foreach遍历。

那问题又来了,map可以使用哪些方法遍历?
第一种:先获取Map集合的全部key的set集合,遍历map的key的Set集合,通过map的key提取对应的value。
第二种:使用foreach遍历把map的key和value打包成Set的key后的这个Set集合
第三种:new一个BiConsumer<key, value>(),然后传入map.foreach()中(和传比较器一样)
第四种:使用lambda表达式
代码实现请看这篇博客:【遍历Map和Set的方式】

2>Set常用方法

Set的常用方法:

方法解释
boolean add(E e)添加元素,但重复元素不会被添加成功
void clear()清空集合
boolean contains(Object o)判断 o 是否在集合中
Iterator iterator()返回迭代器
boolean remove(Object o)删除集合中的 o
int size()返回set中元素的个数
boolean isEmpty()检测set是否为空,空返回true,否则返回false
Object[] toArray()将set中的元素转换为数组返回
boolean containsAll(Collection<?> c)集合c中的元素是否在set中全部存在
是返回true否则返回false
boolean addAll(Collection<? extends E> c)将集合c中的元素添加到set中,可以达到去重的效果
@ Set方法演示

接下来我们用代码来使用Set的toArray和addall方法。

代码如下:

public class Main {
	// 测试使用Set的部分方法
    public static void main(String[] args) {
        // boolean addAll方法
        Set<Integer> set1 = new TreeSet<>();
        set1.add(3);
        set1.add(5);
        Set<Integer> set2 = new TreeSet<>();
        set2.add(1);
        set2.add(5);

        set1.addAll(set2);
        System.out.println(set1);

        // Object[] toArray 方法
        Object[] objects = set1.toArray();
        for (Object o : objects) {
            System.out.print(o + " ");
        }

    }
}

结果:

[1, 3, 5]
1 3 5 

4、Map和Set的实现类概述

在这里插入图片描述


三、TreeMap和TreeSet

1、TreeMap和TreeSet的性质

TreeMap和TreeSet:TreeMapTreeSet
底层结构红黑树红黑树
插入/删除/
查找时间复杂度
O(logn)O(logn)
是否有序关于key有序关于key有序
插入/删除/查找操作需要进行元素比较,按照红黑树的特性进行插入删除和TreeMap一样
比较key必须可以比较,否则会
抛出ClassCastException异常
和TreeMap一样
应用场景需要key有序情况下和TreeMap一样

我们一点一点来分析关于 TreeMap和 TreeSet的这个表中的内容:

1> 底层结构:

红黑树。首先TreeMap和TreeSet从名字上看很明显都和树有关,因为TreeMap和TreeSet的底层都是纯红黑树。

2> 有序性:

关于key有序(指的是平衡二叉搜索树这个结构关于key有顺序,而不是1,2,3,4这样有序)。因为底层是红黑树,即平衡二叉搜索树,插入数据时要根据比较key大小再插入。
中序遍历的结果可以让数据关于key从小到大排列,但中序遍历其实只是一种遍历的方式,和关于key有序无关。

3> 可比较:

由于在插入/删除/查找时,都需要比较key的大小,所以这个key必须是可比较的,如果是一个类比如Student类,那不能直接比较,就需要让它可比较,此时可以让Student类继承Comparable接口,再重写compareTo方法(或者写比较器传入new的TreeMap或TreeSet中)
如果从源码上看也可看出来TreeMap继承了SortedMap接口,而TreeSet继承了SortedSet接口,表示元素应该是可排序的,就得是可比较的才能进行排序。(看第一大点Java中常见集合)
而HashMap和HashSet都没有继承Sorted接口,所以不需要传入的数据可比较,因为它们是通过哈希表实现的。

2、TreeMap以Student的name作为比较

代码如下:

class Student{
    public int id;
    public String name;

    public Student(int id, String name){
        this.id = id;
        this.name = name;
    }
    @Override
    public String toString() {
        return "(id:" + id + ",name:" + name + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map1 = new TreeMap<>();
        map1.put("lisi",12);
        map1.put("wangwu",37);
        map1.put("zhaoliu",89);
        map1.put("xiaosi",42);
        System.out.println("TreeMap:");
        System.out.print("map1:");
        System.out.println(map1);
        
        // 要求key可比较,所以以下的代码中,Student的类必须是可比较的
        Map<Student,Integer> map2 = new TreeMap<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.name.compareTo(o2.name);   // 以Student对象的name为比较对象
            }
        });
        map2.put(new Student(1,"zhangsan"),10);
        map2.put(new Student(2,"lisi"),10);
        //map2.put(null,10); //TreeMap的key不能为null


        System.out.print("map2:");
        System.out.println(map2);
    }
}

结果:

TreeMap:
map1:{lisi=12, wangwu=37, xiaosi=42, zhaoliu=89}
map2:{(id:2,name:lisi)=10, (id:1,name:zhangsan)=10}
  • 以上代码是对TreeMap的演示,因为TreeSet底层是TreeMap,所以是一样的。
  • map1的key和value类型分别为String和Integer,而map2的key和value的类型分别是Student和Integer。
  • 因为map1和map2是TreeMap,所以存入的元素必须是可比较的,对于map1来说,字符串类型可以比较,所以不用传比较器,会根据字母的顺序比较;但对于map2来说,就不能自动比较了,Student对象本身不可比较,但是Student对象的内容可比较,这时我们就需要传入比较器,比如上面的代码就是使用Student的name属性来给Student类型元素比较。

四、HashMap和HashSet

1、HashMap和HashSet的性质

HashMap和HashSet:HashMapHashSet
底层结构哈希桶哈希桶
插入/删除/
查找时间复杂度
O(1)O(1)
是否有序无序无序
插入/删除/查找操作通过哈希函数计算哈希地址1.先计算key哈希地址 2.然后进行插入和删除
覆写自定义类型需要覆写
equals和hashCode方法
和HashMap一样
应用场景key是否有序不关心,需要更高的时间性能和HashMap一样

接下来我们来分析关于HashMap和HashSet的表中内容:

1> 底层结构:

哈希桶。那什么是哈希桶?我们来理一下,哈希表是一种数据结构,它通过哈希函数将key映射到地址(Hash(key)),然后在地址上存储value值。而哈希桶是哈希表的一种实现方式,它使用一个数组来存储键值对,每个数组元素都是一个链表或者红黑树,用于解决哈希冲突。那么怎么得到数组地址的? 操作是通过哈希函数将key映射到数组索引,所以哈希桶是哈希表的一种实现方式。

2> 无序性:

由于哈希函数是随机的,所以哈希表中的元素是无序的。

3> 覆写:

两个对象相等,hashcode必须相等;两个对象不相等,hashCode可能相等,这就会产生哈希冲突,用开散列(示例都是链地址法解决哈希冲突)。
相当于hashCode再通过哈希函数定位数组地址下标,再使用equals去比较当前元素和链表中的key是否相等。所以:两个对象的hashCode一样,equals不一定一样;两个对象的equals一样,hashCode一定一样
注意:在以下示例中,Student就是Map的key,重写hashCode和重写equals都和map的value无关

4> 重写hashCode:

HashMap种在put时,散列函数根据对象的哈希值计算然后找到对应的位置,如果该位置有元素,首先会使用hashCode方法判断新增元素和链表中的元素key是否相同,如果没有重写hashCode方法,那么就是使用object的hashcode方法,这样的话,即使两个对象的key相同,hashCode方法也会认为它们是不同的元素,会有不同的哈希值,但是我们知道HashMap的key是唯一的,那就产生了矛盾。
当重写了hashCode方法后就可以保证相同的key有相同的哈希值(比如Student作为HashMap的key,在Student类中重写hashCode方法,方法中return Objects.hash(id)),就可以保证相同的Student有相同的哈希值,就防止了key相同的两个对象重复插入的问题。在HashMap中没有相同的哈希值,散列函数再根据这个哈希值找到相同的数组下标。因此hashMap中key相同(与value无关)的元素重写hashcode后,哈希值相等

5> 重写equals:

对于equals方法,我们知道HashMap中数据结构是哈希表,可以使用开散列(链地址法)解决哈希冲突,是数组+链表的形式,在我们重写hashCode后,解决了相同对象得到不同哈希值的问题,但是不同的对象可以得到相同的哈希值,从而散列到相同的数组下标下,如果没有重写equals,那就使用的是object的equals方法,内容是:return (this == obj),即比较的地址,那么插入元素的地址和在链表中元素的地址肯定是不一样的,那两个相同的Student类型的元素就会被判定为不相等,除非重写equals,去比较链表中的对象和新增对象是否一样,两个对象不一样就可以进行put了。因此,重写hashCode找到key后,再重写equals(用value作为比较对象,而不是Object类下的比较地址的方法),就可以实现正确的查找


五、总结

  1. Map没有实现Collection接口(单列),Map是双列的,即key-value模型。Map没有实现Iterable接口,所以不能使用foreach遍历,必须先用entrySet方法打包key-value为Set的key,再用foreach来遍历。
  2. TreeMap和TreeSet的key都不能为null,因为插入删除等操作时需要比较,而HashMap和HashSet的key和value都可以为null,因为不需要比较,而是使用哈希表的方式。
  3. 对于TreeMap和HashMap来说,插入两个元素,这两个元素key相同value不同,会怎么样? TreeMap和HashMap中只有后插入的元素。因为TreeMap和HashMap的key唯一,所以相当于把第二个插入的元素的value放在了第一个插入元素的value的位置,即只是更新了value。
    对于TreeSet和HashSet来说,也是一样的,只是无法观察,因为它们只有key,没有value,即结果还是一个数据,还是和第一个插入的key一样。

六、问题

1、HashMap什么时候申请的空间

	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  • 不带容量参数时,是在第一次put时才申请空间,且默认是16(1>>>4)。

2、HashMap传入容量参数时申请空间多大?

	public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  • HashMap传入容量参数时,使用容量参数的在有参构造方法中就申请空间,但是申请空间的大小并不一定是给定的容量参数,而是很接近容量参数同时满足是2的次方的空间。

3、讲一下你理解的hashCode和equals的区别?

  • hashCode加上哈希函数定位数组地址,然后equals比较HashMap的具体的key。

4、当HashMap满了的时候,我们需要注意什么?

  • 每个元素都需要重新哈希

5、什么时候树化?什么时候解树化?

  • 树化:数组长度大于等于64且链表长度大于等于8就会树化(变成红黑树)。
  • 解树化:当一棵红黑树不断删除元素,红黑树的元素个数小于等于6时,会把树转为链表。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学Java的冬瓜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值