MapSet

news2024/11/25 8:22:09

在之前数据结构的学习中,对于数据的查找都是基于给定一个值,通过和序列中的关键字比较而实现的。因此这样的查找效率一般都是更依赖于比较的次数,像直接遍历或二分查找都是如此。而如果我们可以不经过任何比较,只是通过记录的关键字直接得到关键字在序列中的位置,必然会提高我们的查找效率。本文的Map和Set就是实现了这样的查找方式的一种集合。

文章目录

    • 搜索
      • 静态查找
      • 动态查找
    • Map和Set的结构模型
      • 纯key模型
      • key-value模型
    • Map
      • Map的常用方法
      • HashMap和TreeMap
    • Set
      • Set的常用方法
      • HashSet和TreeSet
    • 哈希表
      • 哈希表查找的基本思想
      • 冲突
      • 避免哈希冲突的方法
        • 哈希函数的设计
        • 负载因子的调节
      • 解决哈希冲突的方法
        • 闭散列
          • 线性探测
          • 二次探测
        • 开散列(哈希桶)
      • 代码实现

搜索

静态查找

像直接遍历和二分查找都属于静态查找,即对一个不会再进行插入或删除操作的序列(或集合)进行查找;

动态查找

本文的Map和Set就是可以进行动态查找的集合,即在查找的过程中仍可以对序列(或集合)进行插入或删除的操作;

Map和Set的结构模型

一般我们将进行查找或搜索的数据称为关键字(key),与关键字对应的元素称为值(value),整体可以称作是key-value的键值对;一般有2种模型:

纯key模型

单纯只有关键字;

像一个花名册,查找某个名字是否在该花名册上;Set的结构属于纯key模型;

key-value模型

包括关键字-关键字对应的值两部分;

像一个成绩单,每个名字对应一个分数;Map中存储的数据就是key-value结构的;

Map

Map属于一个接口类,类中存储<k,v>类型的键值对;该类下包括了HashMap和TreeMap2个类,由于TreeMap实现了SortedMap接口,因此TreeMap中的存储是有序的;

Map的常用方法

以代码演示:

public static void main(String[] args) {
        Map<String,Integer> map=new HashMap<>();
        //向HashMap中放入元素
        map.put("hello",12);
        map.put("today",2);
        map.put("happy",5);
        map.put("i",5);
        map.put("log",3);
        //1.返回key对应的value
        Integer t1=map.get("happy");
        System.out.println(t1);     // 5
        //2.返回key对应的value,key不存在时返回默认值
        Integer t2=map.getOrDefault("hehe",100);
        System.out.println(t2);     // 100
        //3.设置key对应的value,不存在的key直接存入,以及存在的key会覆盖之前的value
        map.put("i",10);
        Integer t3=map.get("i");
        System.out.println(t3);     // 10
        //4.删除key对应的映射关系
        map.remove("today");
        //5.返回所有key的不重复的集合
        Set<String> set=map.keySet();
        System.out.println(set);        // [log, happy, i, hello]
        //6.返回所有value的集合
        Collection<Integer> con=map.values();
        System.out.println(con);        // [3, 5, 10, 12]
        //7.返回所有key-value的映射关系
        Set<Map.Entry<String,Integer>> set2=map.entrySet();
        System.out.println(set2);       // [log=3, happy=5, i=10, hello=12]
        //8.判断是否包含key
        boolean t=map.containsKey("happy");
        System.out.println(t);      // true
        //9.判断是否包含value
        boolean f=map.containsValue(100);
        System.out.println(f);      // false

    }

Map是一个接口,不可以直接进行实例化,需要借助HashMap或TreeMap才可以实例化对象;
不可以直接修改Map中的key值;可以先删除再重新插入;

HashMap和TreeMap

  • TreeMap的底层实现是红黑树结构,HashMap的底层实现是哈希桶结构;
  • TreeMap插入/删除/查找的时间复杂度O(logN)(以2为底),HashMap为O(1);
  • TreeMap关于key有序,HashMap无序;
  • TreeMap线程不安全,HashMap线程安全;

Set

Set同样属于一个接口类,继承自Collection类,类中存储纯key类型的元素;

Set的常用方法

同样使用代码进行演示:

public static void main(String[] args) {
        Set<Character> set=new TreeSet<>();
        //添加元素
        set.add('a');
        set.add('m');
        set.add('q');
        // set.add('a');// 添加不成功,不可以添加重复的元素
        //1.判断元素是否在集合中
        set.contains('m');
        //2.删除集合中的某个元素
        set.remove('a');
        //3.返回集合中的元素个数
        int size=set.size();   //2
        System.out.println(size);
        //4.判断集合是否为空
        System.out.println(set.isEmpty());  //false
        //再创建一个集合,加入一些元素到集合里
        Collection<Character> c=new ArrayList<>();
        c.add('a');
        c.add('b');
        c.add('c');
        //5.集合c中的元素在set中是否全部存在
        System.out.println(set.containsAll(c));     //false
        //6.将集合c中的元素添加到set中
        System.out.println(set.addAll(c));          // true
        //7.将集合中的元素转换为数组
        Object [] arr=set.toArray();
        for ( Object i:arr) {
            System.out.println(i);          // a b c m q
        }
        //清空集合
        set.clear();
    }

Set是一个接口类,实例化对象需要借助HashMap或TreeMap;
Set不支持插入重复的元素;
Set不可以插入null;
可以使用Set对集合中的元素进行去重;

HashSet和TreeSet

  • HashSet的底层结构:哈希桶,TreeSet的底层结构:红黑树;
  • TreeSet插入/删除/查找的时间复杂度O(logN)(以2为底),HashSet为O(1);
  • HashSet和TreeSet线程均不安全;;
  • TreeSet关于key有序;

哈希表

前面说到,如果可以在查找元素的过程中,使用记录的关键字直接得到关键字在查找的序列中的位置,就可以加快查找的效率,哈希表就是这样一种存储结构;

哈希表查找的基本思想

在元素的关键字k和元素的存储位置P之间建立一个对应关系,使用P=H(k)表示,则H就是哈希函数,使用这种关系构造出来的结构就称为哈希表;

使用哈希表查找的核心就是哈希函数,即将关键字映射到查找表中的存储位置。

冲突

设置哈希函数为H(key)=key%capacity;

在这里插入图片描述

通过上面的哈希函数对序列中的元素进行存储,发现存在key1!=key2,但H(key1)==H(key2)的情况,即不同关键字通过相同的哈希函数得出了相同的哈希地址,我们称这种现象为哈希冲突(哈希碰撞);称key1与key2为同义词;

尽管冲突现象是难以避免的,但我们还是希望可以找到一个合适的哈希函数的设置方法来尽可能地降低冲突率,即哈希函数的设计;

避免哈希冲突的方法

哈希函数的设计

哈希函数的设计一般都需要遵循简单且易于计算和计算得到的地址要尽量均匀分布2个原则,下面是一些常见的哈希函数:

  1. 直接定址法
    即直接使用关键字求得哈希地址: H(key)=a*key+b;

直接定址法得到的哈希函数简单且分布相对均匀,但使用这种方法时需要事先知道关键字的分布情况;
更适合查找小且连续的元素;

  1. 除留余数法;
    设哈希表的长度为m,取一个小于等于m的最大素数p,得到的哈希函数为: H(key)=key%p (p<=m);

  2. 平方取中法

即首先求出关键字的平方值,再根据需要取平方值的中间几位作为哈希地址;例如关键字2345,对它平方得到5499025,就可以取其中间的990作为哈希地址;

平方取中法更加适合当关键字的分布未知同时位数又不太大的情况;

  1. 分段叠加法(折叠法)

即将关键字从左到右分割成位数相等的几部分,然后将这几部分相加,最后通过哈希表的长度,取结果的后几位为哈希地址;

在这里插入图片描述

折叠法同样使用于关键字的分布未知的情况,但更适合位数较多的情况;

  1. 随机数法
    即采用一个随机函数得到哈希地址,其哈希函数为:H(key)=random(key);

随机数法更适合于关键字的位数不一致的情况;

  1. 数学分析法

数学分析法一般是实现知道关键字的分布,同时关键字的位数要大于哈希表的大小的位数;就从关键字中选取分布比较均匀的几位作为哈希地址;

一般情况下,设计哈希函数需要考虑到下面几个方面的问题:

  • 计算哈希函数的时间;
  • 关键字的长度;
  • 哈希表的大小;
  • 关键字的分布情况;

负载因子的调节

哈希表的负载因子即 r=填入表中的元素的个数/哈希表的长度;

负载因子的大小影响着哈希表中是否有剩余空间,即哈希表是否被装满,也就意味着可能发生冲突的概率大小;一般负载因子的值越大,发生冲突的概率就会越大,因此一个合适的负载因子的取值是重要的。

java中,负载因子的值为0.75;

解决哈希冲突的方法

解决哈希冲突有2种常见的方法,即闭散列和开散列;

闭散列

闭散列,也称为开放定址法;当一个关键字的哈希地址出现冲突时,就以该哈希地址为基础产生另一个哈希地址,若产生的哈希地址又冲突,再以此地址产生下一个新的哈希地址,如此直到元素顺利插入哈希表中。实际就是按照一定规则在哈希表中寻找空闲地址的方式,寻找新的空闲地址的方法主要有下面几种:

线性探测

线性探测,从发生冲突的位置开始,依次向后探测,直到找到下一个空闲的哈希地址;

在这里插入图片描述
由于这种线性探测方法的特殊性,采用了该方法处理哈希冲突的散列表,在进行元素的删除时是使用标记法进行伪删除,以上面的例子为例,也就是避免因为删除3,而找不到13或不容易找到13;

二次探测

线性探测的方式方便但也带来了一个问题,即产生冲突的元素都是堆积在一起的,为了避免这个问题,就有了二次探测的方法。即采用H(i)=(H(0)+i^2)%n来查找下一个空闲的哈希地址;

在这里插入图片描述

开散列(哈希桶)

开散列法又称为链地址法,即首先对关键字根据哈希函数计算哈希地址,若是遇到地址相同的关键字,就将所有地址相同的关键字使用一个链表连接,存储在计算出的哈希地址的位置上;也就是哈希表类似于一个数组,数组的元素可以为链表,每个链表上的元素都是之前计算出的哈希地址相同的元素;

在这里插入图片描述

因此,哈希桶的每个桶中存放的元素都是产生冲突的元素;

代码实现

下面使用代码来实现一个哈希表;

public class HashBack {
    //定义结点结构
    static class Node{
        public int key;
        public int val;
        public Node next;
        public Node (int key,int val){
            this.key=key;
            this.val=val;
        }
    }
    //定义一个存储元素为结点的数组,即哈希表
    public Node[] array;
    //当前哈希表中存放的元素个数
    public int usedSize;
    //设定一个负载因子
    private static final  double DEFAULT_LOAD_FACTOR=0.75;

    /*
    * 实例化一个哈希表(数组)
    * */
    public HashBack(){
        this.array=new Node[10];
    }
    /*
    * 通过key,得到val
    * */
    public int get(int key){
        //通过哈希函数得到哈希地址【即数组的下标】
        int index=key%array.length;
        //通过该下标得到链表的头结点
        Node cur=array[index];
        //遍历链表,查找与key相等的结点
        while (cur!=null){
            if (cur.key==key){
                return cur.val;
            }
            cur=cur.next;
        }
        return -1;
    }

    public void put(int key,int val){
        //创建一个新的结点,待插入到哈希表
        Node node=new Node(key,val);
        //确定存放的位置
        int index=key%array.length;
        /*
        * 遍历当前位置的链表;
        * 遇到一样的key,则替换当前的val;
        * 遍历完没有遇到一样的key,则使用头插法插入该结点;
        * */
        Node cur=array[index];
        while (cur!=null){
            if (cur.key==key){
                cur.val=val;
                return;
            }
            cur=cur.next;
        }
        //插入
        node.next=array[index];
        array[index]=node;
        usedSize++;
        //检查当前哈希表中的元素个数与哈希表大小的占比是否超过了最初设定的负载因子
        if (loadFactor()>=DEFAULT_LOAD_FACTOR){
            //超出负载因子,进行扩容;
            resize();
        }

    }
    /*
    * 进行扩容
    * 扩容时,要为原来哈希表中的元素重新计算在新的哈希表中新的哈希地址
    * */
    private void resize(){
        Node [] tmp=new Node[2*array.length];

        for (int i=0;i<array.length;i++){
            Node cur=array[i];
            while (cur!=null){
                //记录当前结点的下一个指向
                Node curNext=cur.next;
                //计算新的哈希地址
                int index=cur.key%tmp.length;
                //插入
                cur.next=tmp[index];
                tmp[index]=cur;
                cur=curNext;

            }
        }
        array=tmp;
    }
    private double loadFactor(){
        return usedSize*1.0/array.length;
    }
}

插入几个元素进行测试:

public class Test {
    public static void main(String[] args) {
        HashBack hashBack=new HashBack();
        hashBack.put(1,23);
        hashBack.put(3,33);
        hashBack.put(13,56);
        hashBack.put(23,3);
        hashBack.put(7,9);
        hashBack.put(17,23);
    }
}

调试代码,可以看到当前存储的结构:

在这里插入图片描述
上面代码实现的哈希表只是存储基本数据类型的情况,那若是存储引用类型呢?使用下面代码演示;

public class HashBack2 <K,V>{
    static class Node<K,V>{
        public K key;
        public V val;
        public Node<K,V> next;

        public Node (K key,V val){
            this.key=key;
            this.val=val;
        }

    }

    public Node<K,V>[] array=(Node<K, V>[]) new Node[4];

    public int usedSize;

    private static final double DEFAULT_LOAD_FACTOR=0.75;

    public void put(K key,V val) {
        Node<K,V> node = new Node<>(key,val);
        int hash = key.hashCode();
        int index = hash % array.length;

        Node<K,V> cur = array[index];
        while (cur != null) {
            if(cur.key.equals(key)) {
                cur.val = val;
            }
            cur = cur.next;
        }
        node.next = array[index];
        array[index] = node;
        usedSize++;

    }

    private double loadFactor(){
        return usedSize*1.0/array.length;
    }

    private void resize(){
        Node<K,V>[] tmp= (Node<K, V>[]) new Node [2*array.length];

        for (int i=0;i<array.length;i++){
            Node<K,V> cur=array[i];
            while (cur!=null){
                //记录当前结点的下一个指向
                Node<K,V> curNext=cur.next;
                //计算新的哈希地址
                int index=cur.key.hashCode() % tmp.length;
                //插入
                cur.next=tmp[index];
                tmp[index]=cur;
                cur=curNext;

            }
        }
        array=tmp;
    }
    public V get(K key) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while (cur != null) {
            if(cur.key.equals(key)) {
                return cur.val ;
            }
            cur = cur.next;
        }
        return null;
    }
}

over!

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

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

相关文章

外链跳转页功能分析与实现

一个大型的正规网站&#xff0c;增加一个 外链中转页 是有必要的。合理的交互设计&#xff0c;不仅能有效保障用户体验&#xff0c;又能帮助网站收集外链数据&#xff0c;优化运营管理。 目录 1、为什么使用跳转页面来管理外链 1.1、安全性 1.2、搜索引擎优化 1.3、外链数据…

JVM学习(九):堆

一、堆&#xff08;Heap&#xff09;的概述 一个JVM实例只存在一个堆内存&#xff0c;堆也是Java内存管理的核心区域。 Java堆区在JVM启动的时候即被创建&#xff0c;其空间大小也就确定了。是JVM管理的最大一块内存空间。同时&#xff0c;堆内存的大小是可以调节的。《Java虚拟…

ESP32-硬件IIC读取温湿度传感器SHT30

简介 esp32 使用硬件I2C读取温湿度传感器SHT30,例程基于EDP-IDF-4.4.X 的I2C Simple Example 例程修改 工程创建 打开 VSCODE ,通过 查看-- 命令面板&#xff08;快捷键CtrlShiftP&#xff09;&#xff0c;打开 ESP-IDF 的例程后&#xff0c;选择 i2c_simple 例程&#xff0…

深度学习卷积神经网络学习小结

————————————————————————————————————————————— 学习小结&#xff1a; 1&#xff09;深度学习综述&#xff1b;&#xff08;2&#xff09;对卷积神经网络&#xff08;CNN&#xff09;的认识&#xff1b;&#xff08;3&#xff0…

C语言中函数宏的三种封装方式详解

目录 ​编辑 1. 函数宏介绍 3. do{...}while(0) 方式 4. ({}) 方式 5. 总结 1. 函数宏介绍 函数宏&#xff0c;即包含多条语句的宏定义&#xff0c;其通常为某一被频繁调用的功能的语句封装&#xff0c;且不想通过函数方式封装来降低额外的弹栈压栈开销。 函数宏本质上为…

Winform从入门到精通(37)—FolderBrowserDialog(史上最全)

文章目录 前言1、Name2、Description3、RootFolder4、SelectedPath5、ShowNewFolderButton前言 当需要获取一个可以通过用户自由选择路径的时候,这时候就需要FolderBrowserDialog控件 1、Name 获取FolderBrowserDialog对象 2、Description 用于指示对话框的描述,如下: …

leetcode刷题(10)二叉树(4)

各位朋友们&#xff0c;大家五一劳动节快乐啊&#xff0c;在这里祝大家假期玩得愉快&#xff01;但是在玩耍的期间不要忘记了敲代码哦。今天我为大家分享的是二叉树的第四篇&#xff0c;废话不多说&#xff0c;我们一起来看看吧。 文章目录 二叉树的最近公共祖先题目要求做题思…

Stable Diffusion Controlnet V1.1 的14种基础标志用法

用于ControlNet和其他基于注入的SD控件的WebUI扩展。 针对 AUTOMATIC1111 的 Stable Diffusion web UI 网络用户界面的扩展&#xff0c;它可以让网络界面在原始的 Stable Diffusion 模型中添加 ControlNet 条件来生成图像。这种添加是实时的&#xff0c;不需要进行合并。 Con…

【openAI】Whisper如何高效语音转文字(详细教程)

文章目录 前言一、准备二、使用Whisper进行语音转文字三.Whisper转换结果分析总结 前言 语音转文字在许多不同领域都有着广泛的应用。以下是一些例子&#xff1a; 1.字幕制作&#xff1a;语音转文字可以帮助视频制作者快速制作字幕&#xff0c;这在影视行业和网络视频领域非常…

【软件下载】换新电脑记录下下载的软件时所需地址

1.idea https://www.jetbrains.com/zh-cn/idea/download/other.html 2.oracle官方&#xff08;下载jdk时找的&#xff09; https://www.oracle.com/ 3.jdk8 https://www.oracle.com/java/technologies/downloads/ 下拉找到jdk8 切换windows &#xff08;需要注册个oracle账…

TabError: inconsistent use of tabs and spaces in indentation

错误原因是tab制表符和空格混用了。从其他地方复制源码容易出现此错误 解决办法&#xff1a;把处于同级缩进的所有缩进修改统一 比较流行的几个编辑器都能标识tab和空格&#xff0c;比如我用的vscode 用鼠标框选不知道是tab还是空格的部分。 若是空格则显示为上图73行所示的点…

自动化运维工具一Ansible Roles实战

目录 一、Ansible Roles概述 1.1.roles官方的目录结构 1.2.Ansible Roles依赖关系 二、Ansible Roles案例实战 2.1.Ansible Roles NFS服务 2.2 Roles Memcached 2.3 Roles-rsync服务 一、Ansible Roles概述 之前介绍了 Playbook 的使用方法&#xff0c;对于批量任务的部…

C++程序设计——常见C++11新特性

一、列表初始化 1.C98中{}的初始化问题 在C98中&#xff0c;允许使用花括号{}对数组元素进行统一的列表初始化值设定&#xff0c;比如&#xff1a; 但是对于一些自定义类型&#xff0c;就无法使用这样的方式进行初始化了&#xff0c;比如&#xff1a; 就无法通过编译&#xff…

HIT-CSAPP实验二gdb和edb的配置

笔者只是根据自己的电脑进行环境的配置&#xff0c;不一定适配所有的电脑&#xff0c;也不是万能的方法&#xff0c;如果读者使用本人的方法没有配置成功本人深表抱歉。 gdb的使用 通过网上查阅一些资料获得 gdb查看内存和寄存器以及中断设置&#xff08;转&#xff09;_gdb…

关于安装PicGo后启动无界面问题

关于安装PicGo后启动无界面问题 其实我遇到的这个也不算是问题&#xff0c;也挺无语的。 最近为了搭建图床&#xff0c;需要使用PicGo&#xff0c;第一次搭建图床也是第一次使用PicGo。在安装了PicGo后发现启动不了&#xff0c;查看后台发现PicGo在运行着&#xff0c;但是没有界…

数据结构与算法九 树进阶

一 平衡树 之前我们学习过二叉查找树&#xff0c;发现它的查询效率比单纯的链表和数组的查询效率要高很多&#xff0c;大部分情况下&#xff0c;确实是这样的&#xff0c;但不幸的是&#xff0c;在最坏情况下&#xff0c;二叉查找树的性能还是很糟糕。 例如我们依次往二叉查找…

User Experience Design and Information Architecture

&#x1f4a5;(1) What is IA (Information Architecture)? Definition of four sentences I. Information Architecture is "The structure design of shared informaiton environments-共享信息环境的结构设计" II. Information Architecture is "The sy…

ChatGPT提示词工程(三):Summarizing概括总结

目录 一、说明二、安装环境三、概括总结&#xff08;Summarizing&#xff09;1. 简单地概括总结&#xff0c;只有字数限制2. 概括总结需要关注的某些点 四、用“提取”代替“总结”&#xff08;Try "extract" instead of "summarize"&#xff09;五、概括总…

Mysql第二章 多表查询的操作

这里写自定义目录标题 一 外连接与内连接的概念sql99语法实现 默认是内连接sql99语法实现左外连接&#xff0c;把没有部门的员工也查出来sql99语法实现右外连接&#xff0c;把没有人的部门查出来sql99语法实现满外连接&#xff0c;mysql不支持这样写mysql中如果要实现满外连接的…

生成对抗网络原理

GAN的原理 GAN是在2014年由Ian Goodfellow等人提出的&#xff0c;发表在论文“Generative Adversarial Networks”中。 GAN的主要灵感来源于博弈论中零和博弈的思想&#xff0c;应用到深度学习神经网络上来说&#xff0c;就是通过生成网络G&#xff08;Generator&#xff09;和…