【Map与Set】背后的那些事儿】

✨✨hello,愿意点进来的小伙伴们,你们好呐!
🐻🐻系列专栏:【数据结构】
🐲🐲本篇内容: Map 和 Set 背后的数据结构
🐯🐯作者简介:一名现大二的三非编程小白,日复一日,仍需努力。

Map与Set的介绍

Map和Set是一种专门用来搜索的集合数据结构,搜索效率相对于常见的搜索方式来说会较高。而且这两种容器还是天然去除重合元素的。在一些特定的场景下经常被使用到。

然后,这两种数据结构背后会有着千丝万缕的联系,我们在平时可能会经常听到Set的底层是Map来实现的,为什么会这么奇妙呢?那么今天由我来给大家介绍一番。

我们先来看看Map与Set在集合框架中的继承与实现关系
在这里插入图片描述
我们可以看到Map是单独于其他的集合的,然后Set是集合于Collection的,这样子的继承关系会带来什么后果呢?Set的子类有TreeSet与HashSet,Map的子类有HashMap与TreeMap。这这几种数据结构又会有什么不同的方面呢,它们之间又会有什么样的联系的呢?
后面我都会一一介绍。

Map与Set模型

在该数据结构中,我们一般会把要搜索的数据称为关键字(Key),和关键字相对于的值就称为(Value)
我们将其称为 Key-Value键值对
在Map与Set中这种模型的不同的,接下来让我们来看看Map与Set的模型究竟有什么不同呢?

1.Set 纯Key模型:

在Set中,存入的元素只有Key值,没有相对应的值。
这种结构可以使用在 比如说:有一本字典,我们想要从字典中搜索某一个字。

2.Map Key-Value模型:

在Map中,存储的元素就是Key-Value键值对模型的。
这种模型可以使用在 比如说:统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数

TreeMap与TreeSet的事儿

TreeMap与TreeSet的底层是由二叉搜索树以及其优化后的红黑树组成的。
二叉搜索树在上一文中,我以及将其介绍模拟实现出来,这里我就不做过多的介绍。直接来看看TreeSet与TreeMap
然后TreeSet的底层其实是用TreeMap来实现的。,这究竟是为什么呢?接下来让我们来瞧瞧。

TreeMap的介绍:

在这里插入图片描述

TreeMap实现了Map接口,那么它就会有Map接口的方法,接下来我先来介绍一些比较重要的方法。

TreeMap的API:

在这里插入图片描述

我主要来介绍这三种方法。

1. keySet()

该方法将返回集合中的所有的Key,将Key组织起来返回。
在这里插入图片描述

该方法返回值的类型是Set类型的。
在这里插入图片描述
目的就是将集合中的所有Key组织起来使用Set容器存储,并返回。

2. values()

values是一个和keySet对应的一个方法,该方法是将集合中的value组织起来并返回。
在这里插入图片描述
然后返回类型有所不同,是 Collection类型的一个数据。
在这里插入图片描述
目的就是将集合中的所有Value组织起来返回。

重要的entrySet()

该方法是一个很重要的方法,连接着Map与Set。因为有了该方法,所以Map的元素才可以很顺利的遍历出来。
为什么会这样子说呢?

我们来看一下集合框架图。
在这里插入图片描述
我们可以看到Map是一个单独于其他集合类型的一个数据结构,其他单独数据结构都有实现Iterable该接口,这个接口其实就是来遍历集合的一个接口。简单的来讲,只有实现该接口的数据结构才可以使用迭代器遍历。
但是Map没有实现该接口,那该怎么办才能使它具有使用迭代器的性质呢?
Java的设计者就设计了 entrySet()该方法,用来将实现Map的类型转化成Set集合的类型,方便迭代器遍历集合。

在这里插入图片描述

该方法的返回值是Set类型的,该类型中存放 Map.Entry<K, V>类型的数据。
Map.Entry<K, V>就是Map内部实现的用来存放<Key,Value>键值对的映射关系的内部类。在Entry内部类中主要提供了<Key,Value>的获取等方法。

我们大概了解entrySet()的一些基本状况后,我们来看看它是如何使用遍历集合的吧。

1.首先我们要先使用该方法获得Set<Map.Entry<String, Integer>>类型的键值对映射关系。
然后通过返回的对象来调用迭代器进行遍历

TreeMap<String, Integer> map = new TreeMap<>();
        map.put("1",1);
        map.put("2",2);
        map.put("3",3);
        map.put("4",4);
        map.put("5",5);
        map.put("6",6);

        Set<Map.Entry<String, Integer>> entrySet = map.entrySet();

        Iterator<Map.Entry<String, Integer>> iterator = entrySet.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }

2.当然我们也可通过返回的对象去进行调用Map.entry该内部类中的Key Value的返回方法。

TreeMap<String, Integer> map = new TreeMap<>();
        map.put("1",1);
        map.put("2",2);
        map.put("3",3);
        map.put("4",4);
        map.put("5",5);
        map.put("6",6);

        Set<Map.Entry<String, Integer>> entrySet = map.entrySet();

        for (Map.Entry<String, Integer> e :
                entrySet) {
            System.out.println(e.getKey() + "=" + e.getValue());
        }

TreeSet的介绍:

我们为什么会说TreeSet的底层是用TreeMap来实现的呢?
接下来来看看究竟是为什么呢,想要知道这些秘密,我们来浅看一下源码就懂啦

  1. 创建一个TreeSet对象

在这里插入图片描述

2. 调用TreeSet的无参构造器,在该构造器中调用了另一个有参构造器,并传入一个new的TreeMap对象

在这里插入图片描述

3. 在新的构造器中将TreeMap对象向上转型为NavigableMap对象。并赋值给m

在这里插入图片描述

在这里插入图片描述

TreeMap实现了该接口

在这里插入图片描述

4. 用set对象add一个值,会发现将会调用Map的put方法

在这里插入图片描述

在这里插入图片描述

5. 并传入e与PRESENT

在这里插入图片描述
PRESENT 就是一个新 new 的 Object 的类。

总结:
在Set的底层是new了一个Map的对象,Set的add是使用Map的put方法传入Key与一个Object对象

TreeSet的简单方法
在这里插入图片描述

哈希桶的介绍:

哈希桶也被称为哈希表,
在顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中
元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函
数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快
找到该元素。

所以当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若
关键码相等,则搜索成功

这种方法就是哈希(散列)方法
哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)

下面来举个例子:

数据集合【1,8,5,6,4,7】
哈希函数为 : hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

经过哈希得到的结果:
在这里插入图片描述

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

哈希 – 冲突

在哈希过程中,难免会存在两个元素或者多个元素被哈希到哈希表的同一个哈希地址的。这种就被称为哈希冲突,即哈希碰撞。

哈希冲突 – 避免

哈希表底层的数组的容量往往是小于实际上要存储的关键字的数量的,所以冲突是必然的,我们可以做的只是减低冲突。冲突是无法避免的,那应该怎么来减低冲突呢?

冲突减低

哈希函数设计不够合理。就有可能导致冲突的发生几率很高,那么我们应该来设计合理的的哈希函数,虽然哈希函数几乎不会是我们来设计的。但是我们也可以了解一下

常见哈希函数
1. 直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关
键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题:字符串中第一个只出现一次字符
2. 除留余数法–(常用):
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:
Hash(key) = key% p(p<=m),将关键码转换成哈希地址

然而除了哈希函数外的要避免冲突 要了解负载因子,当负载因子超过一定的值时就会调用避免冲突的方法。

负载因子就是 :哈希表中元素的个数 / 哈希表数组的长度

冲突解决 – 闭散列 *开散列

减低哈希冲突的两种方法 – 闭散列 与 开散列
在Java的哈希表是使用开散列来进行哈希冲突的减低的,这里我来重点介绍一些开散列,对于闭散列就做简单的介绍。

闭散列:

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以
把key存放到冲突位置中的“下一个” 空位置中去

那如何寻找下一个空位置呢?
就是使用了线性探测与二次探测

开散列(哈希桶):

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子
集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

在哈希桶中基础就是由数组 + 链表 来实现的
在这里插入图片描述

在这种开散列哈希桶结构中,链表中放的就是每一个发生哈希冲突的元素。然后Java的哈希表就是通过开散列来解决冲突的,当负载因子到达一定的值的时候,就会对哈希表进行扩容。那是怎么扩容的呢,等下一章我来介绍源码中是怎么对哈希表进行扩容的,当哈希表扩容到一定的大小时,就会将哈希表的链式结构转化为红黑树结构。

关于Map

1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
2. Map中存放键值对的Key是唯一的,value是可以重复的。
3. Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
4. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
5. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行
重新插入。

关于Set

1. Set是继承自Collection的一个接口类
2. Set中只存储了key,并且要求key一定要唯一
3. Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的
4. Set最大的功能就是对集合中的元素进行去重
5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础
上维护了一个双向链表来记录元素的插入次序。
6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入7. Set中不能插入null的key。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无满*

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

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

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

打赏作者

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

抵扣说明:

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

余额充值