Java中Map和Set详细介绍,哈希桶的实现

news2024/10/3 19:21:39

大家好呀,前一节我们接触了二叉搜索树,那么紧接着,我们要学习一种十分重要而且也是我们在初阶数据结构中接触的最后一种数据结构—Map和Set,本篇博客将会详细介绍两种数据结构,并且针对哈希表底层实现一个哈希桶,小伙伴们快来一起学起来吧~

一,Map和Set概念与场景

Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。我们以前学习过搜索方式比如二分查找和直接遍历比较适合静态查找,即在查找元素程中,我们不会在对数据进行插入和删除操作了,但是在某些需要需要在查找时 进行一些查找操作的情形下,上述两种数据数据结构便不再适合,因此我们便引出了Map和Set这种数据,他们之间的联系非常紧密,是一种适合进行动态查找的数据结构。

模型简介

在Java中,一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-Value的键值对,所以
模型会有两种:
1. 纯 Key 模型,纯K模型每次枝存储的是一条数据。
2. Key-Value 模型,这种模型存储的是一组数据,这种模型会在数据之间建立联系,可以通过一个数据找到另一个数据
而Map中存储的就是Key-Value的键值对,Set中只存储了Key。

二,Map

Map简介

Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复,Map内部还有一个名字为Entry的接口,也就是说,我们在实Map时,需要把Entry也一并实现,Jvm为我们提供了两种实现了这个接口的类,TreeMap和HashMap(我们可以直接实例化来使用)。首先为大家介绍先为大家介绍Map里的Entry。

3.2 关于Map.Entry<K, V>的说明

在idea中我们可以看到源码

Entry这个接口在Map内部,也就是说,实现Map这个接口的类也一定实现了Entry这个接口,Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,我们可以通过Map的entrySet()方法可以获取Map中所有的键值对,返回一个Set集合,其中每个元素都是一个Map.Entry对象。Map.Entry接口包含了访问和操作键值对的方法,如getKey()、getValue()和setValue(),如下

1,K  getKey() 返回 entry 中的 key
2,V  getValue() 返回 entry 中的 value
3,V  setValue(V value) 将键值对中的value替换为指定value

注意:Map.Entry<K,V>并没有提供设置Key的方法

具体使用方法在讲解Map常用方法的最后一个示范,我们先可以看看TreeMap是怎样实现这个接口的

我们知道TreeMap底层是一颗红黑树,上面这张图也表示TreeMap是以红黑树的方式组织数据的。

Map的常用方法及使用

1.V put(K key, V value) 设置 key 对应的 value

演示:

import java.util.Map;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        Map<String,Integer> treeMap=new TreeMap<>();
        treeMap.put("hello",2);
        treeMap.put("world",3);

    }
}

2.V get(Object key) 返回 key 对应的 value

public class Main {
    public static void main(String[] args) {
        Map<String,Integer> treeMap=new TreeMap<>();
        treeMap.put("hello",2);
        treeMap.put("world",3);
        int val=treeMap.get("hello");
        System.out.println(val);//输出2
    }
}

需要注意的是get()这里方法返回的是一个Integer的值,用int接收自动拆箱,如果用Integer接受则输出null。如果key不存在则抛出异常


3.V getOrDefault(Object key, V defaultValue) 返回 key 对应的 value,key 不存在,返回默认值

和get()   方法相同,只是这个方法可以设置一个默认值,如果key不存在则返回默认的val

public class Main {
    public static void main(String[] args) {
        Map<String,Integer> treeMap=new TreeMap<>();
        treeMap.put("hello",2);
        treeMap.put("world",3);
        int val=treeMap.get("hello");
        System.out.println(val);//输出2
        int val1=treeMap.getOrDefault("abc",100);
        System.out.println(val1);//"abc"不存在,输出100
    }
}


4.V remove(Object key) 删除 key 对应的映射关系

简单来说就是删除Key对应的一组数据,十分简单

5.boolean containsKey(Object key) 判断是否包含 key
6.boolean containsValue(Object value) 判断是否包含 value

这两个方法就是判断是否包含某个key或者val,理解起来没有难度  这里就不作演示了


7.Set<K> keySet() 返回所有 key 的不重复集合

也就是把所有Map中的Key值组织成一个Set,Set我们后面还会继续介绍,还可以用一个Set的对象来接收,里面存放的就是所有Map里的Key值的一个集合

组织起来后我们当然就可以用Set的方法来对他进行操作


8.Collection<V> values() 返回所有 value 的可重复集合

有了可以组织Key值的Set,那么自然有组织Values的方法,不过,这个方法是把所有value值组织成一个Collection对象,这个我们从他的返回值中可以清楚的反映出来,至于Collection嘛,他是一个类,这里我再放出这张图

当然我们也可以用一个对象来接收

Collection<Integer> collection=treeMap.values();

9.Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射关系

这个方法和我们上面所说的的Entry接口有关,它主要是把Map的一个个节点组织成一个Set,注意是节点,这个从他的返回值可以看出,下面演示用法

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        Map<String,Integer> treeMap=new TreeMap<>();
        treeMap.put("hello",2);
        treeMap.put("world",3);
        Set<Map.Entry<String, Integer>> set=treeMap.entrySet();
        //试试怎么用for—each循环遍历
        for (Map.Entry<String, Integer> s : set) {//注意s的类型
            //这里就可以调用上面entry介绍的三种方法
            s.getKey();
            s.getValue();
            s.setValue(1);
        }
    }
}

迭代器遍历方式

学习了entrySet方法下面额外为大家介绍一种用迭代器遍历Map的方式,

我们可以看出Set继承了Iterator接口,但是Map没有,所以我们不能直接用迭代器的方式来遍历Map,但是通过上面的方法。我们可以先把Map转化为Set,再进行遍历

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> treeMap = new TreeMap<>();
        treeMap.put("hello", 2);
        treeMap.put("world", 3);
        Set<Map.Entry<String, Integer>> set = treeMap.entrySet();
        Iterator<Map.Entry<String, Integer>> it = set.iterator();
        while (it.hasNext()) {
            System.out.println(it.next() + " ");
        }
    }
}

Map的方法就差不多说到这里了

注意事项:


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

三,Set

Set的常用方法

Set与Map主要的不同有两点:Set是继承自Collection的接口类,Set中只存储了Key。其余的很多使用方法很相似,这里简略概述

1.boolean add(E e) 添加元素,但重复元素不会被添加成功
2.void clear() 清空集合
3.boolean contains(Object o) 判断 o 是否在集合中
4.boolean remove(Object o) 删除集合中的 o
5.int size() 返回set中元素的个数
6.boolean isEmpty() 检测set是否为空,空返回true,否则返回false

上面六个方法很基础也很简单,这里一起演示

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        boolean boo=treeSet.contains("hello");//输出true,不存在则输出false
        int size=treeSet.size();//2
        treeSet.remove("hello");
        System.out.println(treeSet.isEmpty());
        treeSet.contains("hello");//false
    }
}

7.Iterator<E> iterator() 返回迭代器

和上面介绍过的相同,主要是迭代器遍历

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        Iterator<String> iterator=treeSet.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next()+" ");
        }
    }
}


8,Object[] toArray() 将set中的元素转换为数组返回

需要注意返回的是Object对象需要强转

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        String[] strings= (String[]) treeSet.toArray();
        
    }
}


9.boolean containsAll(Collection<?> c) 集合c中的元素是否在set中全部存在,是返回true,否则返回false

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        Set<String> treeSet1=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("abc");
        treeSet.contains("hello");
        treeSet.containsAll(treeSet1);

    }
}


10.boolean addAll(Collection<? extendsE> c) 将集合c中的元素添加到set中,可以达到去重的效果

注意,这个集合必须继承自Collection

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        Set<String> treeSet1=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("abc");
        treeSet.addAll(treeSet1);//有去重效果
        for (String s:treeSet) {
            System.out.println(s);
        }
    }
}

注意事项

1. Set是继承自Collection的一个接口类
2. Set中只存储了key,并且要求key一定要唯一
3. TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的

(源码)


4. Set最大的功能就是对集合中的元素进行去重
5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
7. TreeSet中不能插入null的key,HashSet可以

四,哈希桶的实现

实现哈希表有多种方式,要点还是在于如何解决哈希冲突,这里采用哈希桶的方式,用数组-链表结合来实现

哈希冲突

定义:(百度的)

哈希冲突‌是指在哈希表中,两个或更多个不同的键被映射到了同一个哈希桶的情况这种情况可能会导致数据丢失或者检索效率下降,因为不同的键被映射到了同一个位置,需要额外的操作来处理这种冲突。

简单来说,我们再用哈希表(注:哈希表可以是数组,链表,树等数据结构)存储数据时,因为要根据要插入的数值大小来确定这个树的存储位置,就有可能会出现多个数据映射到同一位置的情况,举个例子:

我们定义一个长度为10的哈希表(这里采用数组)并以数据在数组中的下标=数据大小%数组长度的方式来确定数组位置,那么这时候,我们可能会遇到一个问题,如图

当然,为了解决这个问题也有很多方法,我们这里采用哈希桶的方法,即采用数组+链表的方式来存储数据

这样,哈希冲突便得到了有效解决,当然,如果数据过多的时候,我们就会对数组进行扩容,一般来说,当数据个数/数组长度 >0.75时,我们对数组进行扩容。这里的0.75我们称作负载因子,可以自己定义,一般在0.75左右最佳

代码实现

初始准备:

因为我们采用数组+链表模拟,所以我们需要定义链表节点的值和存放这个链表的数组

class HashBuck{
    class Node
    {
        int key;//哈希表是KEY-VAl模型
        int val;
        Node next;
        Node(int val,int key){
            this.val = val;
            this.key = key;
        }
    }
    Node[] arrays=new Node[10];//初始数组长度
    int useSize=0;//数组里面元素个数
  }
}

插入操作

假设我们在传入值为key和关键码为val的值,我们一般以key%数组长度来确定此值在数组中的位置,在插入时,还需要判断数组中是否有值为val的值,因为哈希表中的值不能重复,所以如果有值为val的值时,我们便更新它的关键码为新的key值,否则,我们在数组对应位置中的链表中头插或者尾插一个新的节点,这里选择头插或者尾插均可,不是我们关注的重点,最后别忘记了useSize++


    public void Insert(int val,int key){
        int index=key/ arrays.length;//确定数据在数组中的位置
        Node cur=arrays[index];//用于遍历数组中的链表
        while(cur!=null){
            if(cur.val==val){//有值为val的节点则更新它的key后走人~
                cur.key=key;
                return;
            }
            cur=cur.next;
        }
        Node node=new Node(val, key);
       //这里选择头插的方式
        node.next=arrays[index];
        arrays[index]=node;
        useSize++;
}

在插入操作基本完成后,这个时候就需要我们关注哈希冲突的问题,当负载因子大于0.75时,我们就对数组进行扩容,写一个计算负载因子的方法,如果超过负载因子,就对数组进行扩容。但这个时候千万不能像顺序表那样扩容,因为数组的变化会引起代码中的index的变化(index=key%数组长度),这个时候我们需要进行遍历和重新哈希,来达到调整的目的,此时我们可以重新封装一个方法

计算负载因子

private static final double LoadFactor=0.75f;
private double doLoadFactor(){
        return useSize*1.0/arrays.length;//useSize*1.0才能计算出小数
    }

然后就可以来调整数组大小了,调整过后需要重新哈希

重新哈希

 private void resize() {
        Node[] array=new Node[2*arrays.length];//新的数组
        for (int i = 0; i <array.length; i++) {//开始遍历
            Node cur=arrays[i];
            while(cur!=null){
                 Node curN=cur.next;//必须要先记录下下一个位置,否则可能会因为下面的操作打乱
                int index=cur.key/array.length;//新的index
                 cur.next=array[index];
                 array[index]=cur;
                 cur=curN;
            }
        }
          arrays=array;//新的数组给旧的数组
    }

最后提一句,在JVM中,当数组长度超过64或者链表长度超过8时,就会把哈希桶调整成树

完整代码

class HashBuck{
    class Node
    {
        int val;
        int key;
        Node next;
        Node(int val,int key){
            this.val = val;
            this.key = key;
        }
    }
    Node[] arrays=new Node[10];
    int useSize=0;
    private static final double LoadFactor=0.75f;
//插入
    public void Insert(int val,int key){
        int index=key/ arrays.length;
        Node cur=arrays[index];
        while(cur!=null){
            if(cur.val==val){
                cur.key=key;
                return;
            }
            cur=cur.next;
        }
        Node node=new Node(val, key);
        node.next=arrays[index];
        arrays[index]=node;
        useSize++;

        if(doLoadFactor()>0.75){
            resize();
        }
    }
//重新哈希
    private void resize() {
        Node[] array=new Node[2*arrays.length];
        for (int i = 0; i <array.length; i++) {
            Node cur=arrays[i];
            while(cur!=null){
                 Node curN=cur.next;
                int index=cur.key/array.length;
                 cur.next=array[index];
                 array[index]=cur;
                 cur=curN;
            }
        }
          arrays=array;
    }
//计算负载因子
    private double doLoadFactor(){
        return useSize*1.0/arrays.length;
    }
}

以上就是全部内容了,感谢大家支持。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2186741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于元神操作系统实现NTFS文件操作(三)

1. 背景 本文主要介绍DBR的读取和解析&#xff0c;并提供了基于元神操作系统的实现代码。由于解析DBR的目的是定位到NTFS磁盘分区的元文件$Root进行文件操作&#xff0c;所以只解析了少量的部分&#xff0c;其它部分可以参考相关文档进行理解。 DBR存在于磁盘分区的第一个扇区…

《数据结构》--链表【包含跳表概念】

不知道大家对链表熟悉还是陌生&#xff0c;我们秉着基础不牢&#xff0c;地动山摇的原则&#xff0c;会一点点的介绍链表的&#xff0c;毕竟链表涉及的链式存储也很重要的。在这之前&#xff0c;我们认识过顺序存储的顺序表&#xff0c;它其实就是一个特殊的数组。那链表到底是…

树莓派 AI 摄像头(Raspberry Pi AI Camera)教程

系列文章目录 前言 人们使用 Raspberry Pi 产品构建人工智能项目的时间几乎与我们生产 Raspberry Pi 的时间一样长。随着我们发布功能越来越强大的设备&#xff0c;我们能够支持的原生应用范围也在不断扩大&#xff1b;但无论哪一代产品&#xff0c;总会有一些工作负载需要外部…

SpringBoot介绍及整合Mybatis Plus

目录 SpringBoot背景及特点 SpringBoot整合Mybatis Plus SpringBoot背景及特点 SpringBoot的设计目是抛弃之前Spring、SpringMVC繁杂的配置过程&#xff0c;简化开发过程。之前的Spring框架需要大量的手动配置&#xff0c;包括XML配置文件或Java配置类&#xff0c;配置过程繁…

深入理解 Git 一个开发者的必备工具

深入理解 Git 一个开发者的必备工具 演示地址 演示地址 获取更多 获取更多 在现代软件开发中&#xff0c;版本控制系统扮演着至关重要的角色。其中&#xff0c;Git 是最流行的选择之一。无论你是新手还是有经验的开发者&#xff0c;了解 Git 的基本概念和使用方法都能大大提…

YOLO v11实时目标检测3:训练数据集格式说明

一、Yolov11简介 YOLOv11 是 YOLO 系列的最新版本&#xff0c;它不仅在目标检测方面表现出色&#xff0c;还引入了对象分割和多目标跟踪的功能。本文将介绍如何使用 YOLOv11 进行人流统计、车流统计以及跟踪的实际应用。 二、Yolo v11训练数据集格式说明 2.1 数据组织&#…

SAT分离轴定理的c++/python实现

分离轴定理的c/python实现 现在要对BEV模型检查出来的车辆做NMS&#xff0c;把3d框的平面属性获取到后&#xff0c;配合旋转角度投影到地面就是2D图形。 开始碰撞检测&#xff0c;判断是否重叠&#xff0c;保留置信度高的框就行。 原理 分离轴定理&#xff08;Separating A…

(C语言贪吃蛇)11.贪吃蛇方向移动和刷新界面一起实现面临的问题

目录 前言 实现效果 支持方向变换 修改默认效果 如何修改 总结 前言 我们上节实现了不需要按下右键就可以是贪吃蛇自发的向右移动&#xff0c;本节我们主要来解决贪吃蛇方向移动和刷新界面所遇到的问题。 实现效果 上图是我们希望实现的效果&#xff0c;我们可以自发地控…

Go 进阶:Go + gin 极速搭建 EcommerceSys 电商系统

Go 进阶:Go + gin 极速搭建 EcommerceSys 电商系统 前言 本章节适合有一定基础的 Golang 初学者,通过简单的项目实践来加深对 Golang 的基本语法和 Web 开发的理解。 具体请联系作者 项目结构 项目流程图 技术栈 项目结构 项目路由 4. 项目模型 项目初始化 初始化项目文…

归并排序【C语言版-笔记】

目录 一、概念二、排序流程理解三、代码实现3.1主调函数3.2 merge函数 四、性能分析 一、概念 归并是一种算法思想&#xff0c;是将两个或两个一上的有序表合并成一个长度较大的有序表。若一开始无序表中有n个元素&#xff0c;可以把n个元素看作n个有序表&#xff0c;把它们两…

Java中数据转换以及字符串的“+”操作

隐式转换&#xff08;自动类型转换&#xff09; 较小范围的数据类型转成较大范围的数据类型 强制转换&#xff08;显式转换&#xff09; 将数据范围大的数据类型转换为数据范围小的数据类型 基本数据类型之间的转换 当需要将一个较大的数据类型&#xff08;如float或double…

Linux:进程控制(一)

目录 一、写时拷贝 1.创建子进程 2.写时拷贝 二、进程终止 1.函数返回值 2.错误码 3.异常退出 4.exit 5._exit 一、写时拷贝 父子进程&#xff0c;代码共享&#xff0c;不作写入操作时&#xff0c;数据也是共享的&#xff0c;当任意一方试图写入&#xff0c;便通过写时拷…

影刀RPA实战:excel相关图片操作指令解

1.实战目标 excel是工作中必不缺少的工具&#xff0c;今天我们继续使用影刀RPA来实现excel操作的便利性&#xff0c;让影刀自动化来帮我们完成工作。 2.单元格填充图片 2.1 指令说明 功能&#xff1a;向 Excel 单元格插入本地图片或网络图片&#xff0c;支持Office和WPS&…

波阻抗,是电场矢量的模值/磁场矢量的模值

波阻抗是电场复振幅除以磁场复振幅&#xff0c;最后只与介质με有关 所以磁场可用电场强度表示&#xff08;利用波阻抗&#xff09; 问题&#xff0c;复振幅是矢量&#xff0c;波阻抗的定义是复振幅的比值&#xff1f;答案&#xff1a;不是&#xff0c;很明显&#xff0c;波阻…

Web3 游戏周报(9.22 - 9.28)

回顾上周的区块链游戏概况&#xff0c;查看 Footprint Analytics 与 ABGA 最新发布的数据报告。 【9.22-9.28】Web3 游戏行业动态&#xff1a; Axie Infinity 将 Fortune Slips 的冷却时间缩短至 24 小时&#xff0c;从而提高玩家的收入。 Web3 游戏开发商 Darkbright Studios…

Pikachu-Sql Inject-搜索型注入

MySQL的搜索语句&#xff1a; select * from table where column like %text%&#xff1b; 如&#xff1a;使用引号闭合左边的引号&#xff0c; or 11 把所有数据查询出来&#xff1b; # 注释掉后面的 引号等&#xff1b; test or 11# 查询出结果&#xff1a; 注入的核心点…

Cloneable接口(浅拷贝和深拷贝的区别)

前言 Object类中存在这一个clone方法&#xff0c;调用这个方法可以创建一个对象的“拷贝”。但是想要合法调用clone方法&#xff0c;必须要先实现Clonable接口&#xff0c;否则就会抛出CloneNotSupportedException异常。 1 Cloneable接口 //Cloneable接口声明 public interf…

CentOS 7文件系统

从centos7开始&#xff0c;默认的文件系统从ext4变成了XFS。随着虚拟化的应用越来越广泛&#xff0c;作为虚拟化磁盘来源的大文件&#xff08;单个文件几GB级别&#xff09;越来越常见。 1.XFS组成部分&#xff1a; XFS文件系统在数据的分布上主要划分为三部分&#xff1a;数据…

QT篇:QT介绍

一.QT概述 Qt 是一个跨平台的应用程序和用户界面框架&#xff0c;用于开发图形用户界面&#xff08;GUI&#xff09;应用程序以及命令行工 具。它最初由挪威的 Trolltech &#xff08;奇趣科技&#xff09;公司开发&#xff0c;现在由 Qt Company 维护&#xff0c;2020年12月8…

如何在网格中模拟腐烂扩散:如何使用广度优先搜索(BFS)解题

问题描述 你需要在一个二维的网格中处理橘子的腐烂扩散过程&#xff0c;网格中的每个单元格可以有三种状态&#xff1a; 0&#xff1a;表示空格&#xff0c;没有橘子。1&#xff1a;表示一个新鲜的橘子。2&#xff1a;表示一个腐烂的橘子&#xff0c;它可以在 1 分钟内让上下…