数据结构Map-Set和哈希表

2023-09-13 17:43:46

目录

概念

模型

Map

Map的常用方法

对于Map的总结

Set

Set的常见方法

关于Set的总结

哈希表

概念

冲突

概念

哈希函数设计原则

常见的哈希函数

1.直接定制法(常用)

2.除留余数法(常用)

3.平方取中法

4.折叠法

5.随机数法

6.数学分析法

冲突避免-负载因子调节

冲突-解决

闭散列

线性探测

二次探测

开散列/哈希桶

三个例题理解Map和Set

复制带随机指针的链表


概念

Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关,比如TreeMap和TreeSet的效率一般是不如HashMap和HashSet的.

模型

一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称为Key-value的键值对,所以对应的模型会有两种:纯Key模型和Key-Value模型.

Map中存储的就是key-value模型,Set中只存储了Key.

Map

Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,而且K一定是唯一的,不能重复.

TreeMap实现了SortedMap接口,所以TreeMap中的元素一定是可比较的.

Map的常用方法

方法演示

TreeMap<String,Integer> map = new TreeMap<>();
        map.put("hello",2);
        map.put("abc",4);
        //返回key对应的value
        //如果map中没有hello,会返回null
        Integer e =map.get("hello");
        System.out.println(e);
        //有key返回对应值,没有返回默认值
        Integer s = map.getOrDefault("hello2",520);
        System.out.println(s);
        //取出key值,进行组织
        //用Set接收
        Set<String> set = map.keySet();
        System.out.println(set);
        //取出value值,进行组织
        //用Collection接收
        Collection<Integer> collection = map.values();
        System.out.println(collection);
        //containsKey,判断是否包含key
        boolean t = map.containsKey("hello");
        System.out.println(t);
        //containsValue,判断是否包含value
        boolean r = map.containsValue("88");
        System.out.println(r);

entrySet方法

Map.Entry<K,V>是Map内部实现的用来存放<key,value>键值对映射关系的内部类,该内部类中主要提供了下列方法:

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

此方法就是将<"hello",2>作为一个整体存放在set当中,这个整体的类型Map.Entry<String,Integer>.

由于Map并没有实现Iterable接口,所以for-each无法直接遍历Map.

这个方法相当于是提供了遍历Map的方法.


对于Map的总结

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

Set

Set 与Map的不同主要有两点:Set是继承自Collection的接口类,Set中只存储了Key.

Set的常见方法

在TreeSet当中存储元素的时候,其实是存在了TreeMap当中,但是value是默认的一个值.

不管存哪个key,value永远是PRESENT这个值.

关于Set的总结

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

哈希表

以往,我们在一些记录当中查找指定数据的时候,会有以下几种方法

  1. 把数据存储到数组当中,然后遍历数组去查找.时间复杂度是O(N)
  2. 假设数据是有序的情况下,那么二分查找是最快的,时间复杂度可以达到O(lognN).
  3. 我们也可以利用搜索树,可以达到O(logN).

现在,有一种可以将时间复杂度做到O(1)的结构,那就是哈希表.

概念

不经过任何比较,一次直接从表中得到要搜索的元素.如果构造一种存储结构,通过某种函数使元素的存储位置和它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快的找到该元素.

该港是即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希函数,构造出来的结构称为哈希表(HashTable)(散列表).

例如:数据集合{176459}

哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快.但是,如果以此方式,向集合里插入14,会出现位置占用的问题,这就出现了冲突.

冲突

概念

不同关键字通过哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或者哈希碰撞.

把具有不同关键码而具有相同哈希地址的数据元素称为"同义词".

冲突避免-巧妙设计哈希函数

我们要明确,由于哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致冲突的发生是必然的,我们能做的应该是尽量的降低冲突率.

哈希函数设计原则

引起哈希冲突的一个原因可能是:哈希函数设计不合理.

设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见的哈希函数

1.直接定制法(常用)
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况.
例题
直接定制法例题
class Solution {
    public int firstUniqChar(String s) {
            int[] array = new int[26];
            for(int i = 0; i < s.length();i++){
                char ch = s.charAt(i);
                array[ch-'a']++;
            }
            for(int i = 0; i < s.length();i++){
                char ch = s.charAt(i);
                if(array[ch-'a'] == 1){
                    return i;
                }
            }
            return -1;
    }
}
2.除留余数法(常用)
设散列表中允许的地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址.
3.平方取中法
假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 227 作为哈希地址; 再比如关键字为 4321 ,对它平方就是18671041 ,抽取中间的 3 671( 710)作为哈希 地址.
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况.
4.折叠法
折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5.随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中 random 为随机数函数。
通常应用于关键字长度不等时采用此法
6.数学分析法
设有 n d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址.
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是 相同的,那么我们可以
选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转 (
1234 改成 4321) 、右环位移 ( 1234 改成 4123) 、左环移位、前两数与后两数叠加 ( 1234 改成 12+34=46) 等方
法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均 匀的情况.

冲突避免-负载因子调节

散列表的载荷因子定义为:α =填入表中的元素个数/散列表的长度.

α 是散列表装满程度的标志因子.由于表长是定值,α 与填入表中的元素个数成正比,α 越大,表明填入表中的元素越多,产生冲突的可能性就越大.

负载因子和冲突率的关系演示

当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率.

已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组大小.

α 与哈希表数组的长度是成反比的.


冲突-解决

解决哈希冲突的两种常见方式:闭散列和开散列.

闭散列

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

寻找下一个空位置有两种方法:线性探测法和二次探测法.

线性探测

从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止.

比如要插入44,先通过哈希函数获取待插入元素在哈希表中的位置,如果该位置没有元素就直接插入新元素,显然这里4占据了位置;此时元素发生哈希冲突,则使用线性探测法找到下一个空位置,下标为8的位置,插入44.

需要注意的是,采用此种方法处理哈希冲突的时候,不能随便物理删除哈希表中已有的元素,若直接删除会影响其他元素的搜索.比如删除4,如果直接删除4,那么对查找44就会产生影响.因此线性探测采用标记的伪删除法来删除一个元素.

二次探测
研究表明:当表的长度为质数且表装载因子 a 不超过 0.5 时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a 不超过 0.5 ,如果超出必须考虑增容.
所以,闭散列最大的缺陷就是空间利用率低.

开散列/哈希桶

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

开散列的每个桶中放的都是发生哈希冲突的元素.开散列可以认为是把一个在大集合中的搜索问题转化为在小集合中搜索了.

开散列采取数组+链表的方式来组织数据.当数组长度超过64并且链表长度超过8的时候,链表就会变为红黑树.

JDK1.7及以前,链表的插入采取的是头插法,JDK1.8开始采用尾插法.

开散列法也是Java中用来解决哈希冲突所采取的方法.


三个例题理解Map和Set

//统计10w个数据中,不重复的数据(去重)
    public static void func1(int[] array) {
        HashSet<Integer> set = new HashSet<>();
        for (int i = 0; i < array.length; i++) {
            set.add(array[i]);
        }
        System.out.println(set);
    }

    //2、统计10W个数据当中,第一个重复的数据?
    public static void func2(int[] array){
        HashSet<Integer> set = new HashSet<>();
        for (int i = 0; i < array.length; i++) {
            if (!set.contains(array[i])){
                set.add(array[i]);
            }else {
                System.out.println(array[i]);
                return;
            }
        }
    }

    //3、统计10W个数据当中,每个数据出现的次数? 对应的关系
    public static void func3(int[] array){
        HashMap<Integer,Integer> map = new HashMap<>();
        for (int i = 0; i < array.length; i++) {
            //第一次存入
            if (map.get(array[i]) == null){
                map.put(array[i],1);
            }else {
                int val = map.get(array[i]);
                map.put(array[i],val+1);
            }
        }
        Set<Map.Entry<Integer,Integer>> set = map.entrySet();
        for (Map.Entry<Integer,Integer> entry:set) {
            System.out.println(entry.getKey() + "出现了" + entry.getValue()+"次");
        }
    }

复制带随机指针的链表

用Map去做,会变得非常容易.

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        //用Map存储新老节点的对应关系
        HashMap<Node,Node> map = new HashMap<>();
        Node cur = head;
        while(cur != null){
            Node node = new Node(cur.val);
            map.put(cur,node);
            cur = cur.next;
        }
        cur = head;
        while(cur != null){
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur = cur.next;
        }
        return map.get(head);
    }
}

两个对象的hashcode一样,equals不一定一样.

两个对象的equals一样,hashcode一定一样.

java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key equals 方 法。所以如果要用自定义类作为 HashMap key 或者 HashSet 的值, 必须覆写 hashCode equals ,而且要做到 equals 相等的对象, hashCode 一定是一致的。

更多推荐

融合柯西变异和自适应莱维飞行的布谷鸟优化算法,改进布谷鸟,MATLAB代码

经常有小伙伴后台留言问:作者改进的算法可不可以用来写论文呀?回答是:当然可以!且不用加引用!如果我的文章能帮助到大家写论文,那是作者的荣幸呀!布谷鸟优化算法是一个非常经典的优化算法,直到今天还有不少人研究对其改进。今天为大家带来一期由小淘自行改进的布谷鸟优化算法---融合柯西变异和自适应莱维飞行的布谷鸟优化算法(Cau

正则表达式以及python的re模块介绍

正则表达式字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在。比如判断一个字符串是否是合法的Email地址,虽然可以编程提取@前后的子串,再分别判断是否是单词和域名,但这样做不但麻烦,而且代码难以复用。正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义

【PX4】Ubuntu20.04+ROS Noetic 配置PX4-v1.12.2和Gazebo11联合仿真环境【教程】

【PX4】Ubuntu20.04+ROSNoetic配置PX4-v-v1.12.2和Gazebo11联合仿真环境【教程】文章目录【PX4】Ubuntu20.04+ROSNoetic配置PX4-v-v1.12.2和Gazebo11联合仿真环境【教程】0.安装Ubuntu+ROS1.安装依赖2.安装QGC地面站3.配置PX

【JavaSE专栏53】Java集合类HashMap解析,基于哈希表的键值对存储结构

作者主页:Designer小郑作者简介:3年JAVA全栈开发经验,专注JAVA技术、系统定制、远程指导,致力于企业数字化转型,CSDN学院、蓝桥云课认证讲师。主打方向:Vue、SpringBoot、微信小程序本文讲解了Java中集合类HashMap的语法、使用说明和应用场景,并给出了样例代码。目录一、什么是HashMa

【JavaSE专栏51】Java集合类HashSet解析,基于哈希表无序非重元素集合

作者主页:Designer小郑作者简介:3年JAVA全栈开发经验,专注JAVA技术、系统定制、远程指导,致力于企业数字化转型,CSDN学院、蓝桥云课认证讲师。主打方向:Vue、SpringBoot、微信小程序本文讲解了Java中集合类HashSet的语法、使用说明和应用场景,并给出了样例代码。目录一、什么是HashSe

【Linux】进程间通信

目录一、进程间通信的目的二、管道通信1.匿名管道2.命名管道①.创建命名管道②.使用命名管道三、systemV标准进程间通信1.共享内存①常用接口②共享内存的内核数据结构③查看删除共享内存一、进程间通信的目的数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另

【EI检索会议】第四届智能电网与能源工程国际研讨会(SGEE 2023)

第四届智能电网与能源工程国际研讨会(SGEE2023)20234thInternationalConferenceonSmartGridandEnergyEngineering继往届SGEE年度系列会议的成功举办,所录用论文已完成出版及检索。第四届智能电网与能源工程国际研讨会将于2023年11月24日-26日在河南郑州

如何构建一个 NodeJS 影院微服务并使用 Docker 部署

文章目录前言什么是微服务?构建电影目录微服务构建微服务从NodeJS连接到MongoDB数据库总结前言如何构建一个NodeJS影院微服务并使用Docker部署。在这个系列中,将构建一个NodeJS微服务,并使用DockerSwarm集群进行部署。以下是将要使用的工具:NodeJS版本7.2.0MongoDB3.4.1D

ChatGPT:字符串操作问题——提取包含括号的字符串中的题干内容

ChatGPT:字符串操作问题——提取包含括号的字符串中的题干内容Stringtitle=p.text().split(“(”)[0];为什么会报错ChatGPT:在这段代码中,您正在使用Java处理一个字符串(假设是HTML或文本),尝试将其分割成两部分,其中一个是从左括号"("之前的内容。这段代码的目的是提取标题,

消息队列(三):硬盘管理

我在第一章提到过:关于持久化那一块,在内存存储主要是获取比较方便,在硬盘上存储主要是为了长时间存储。关于内存管理又分两部分:数据库管理和文件管理(主要是和消息有关)。硬盘管理硬盘中的数据库管理其实在上一章的建库建表说过了,就这么几步,SQLite+MyBatis几十行代码就搞定了除此之外就是对其进行一个封装即可。这里的

K8S名称空间和资源配额

Kubernetes支持多个虚拟集群,底层依赖于同一个物理集群。这些虚拟集群被称为名称空间。名称空间namespace是k8s集群级别的资源,可以给不同的用户、租户、环境或项目创建对应的名称空间,例如,可以为test、dev、prod环境分别创建各自的命名空间。当多个用户或团队共享具有固定节点数目的集群时,人们会担心有

热文推荐