Java数据结构第二十三期:Map与Set的高效应用之道(二)

news2025/3/16 18:11:00

专栏:Java数据结构秘籍

个人主页:手握风云

目录

一、哈希表

1.1. 概念

1.2. 冲突

1.3. 避免冲突

1.4. 解决冲突

1.5. 实现

二、OJ练习

2.1. 只出现一次的数字

2.2. 随机链表的复制

 2.3. 宝石与石头


一、哈希表

1.1. 概念

        顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找⼀个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(n),平衡树中为树的⾼度,即O(n),搜索的效率取决于搜索过程中元素的比较次数。

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

        当向该结构中,根据待插⼊元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放;对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。该方式即为哈希(散列)方法,哈希方法中使⽤的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。

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

        我们设一个整数集合{1,7,6,4,5,9},把capacity设置为10,那我们就可以按照下图来存储。如果我们再想存放一个元素12,我们可以直接通过哈希函数存进下标2中,要想搜索,直接通过2下标来找到12,这样时间复杂度为O(n),从而提高效率。

1.2. 冲突

        不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码⽽具有相同哈希地址的数据元素称为“同义词”。例如我们要想存放一个14,通过上面的哈希函数应该存到4下标,但此时4下标已经存了一个4,就会造成哈希冲突。

        出现了哈希冲突,我们就要想办法避免冲突。由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,就会导致冲突的发⽣是必然的,但我们能做的应该是尽量的降低冲突率。

1.3. 避免冲突

        第一种方法可以设计合理的哈希函数。设计原则:定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间;计算出来的地址能均匀分布在整个空间中;比较简单。

        直接订制法:取关键字的某个线性函数为散列地址:Hash(Key) = A*Key + B。优点:简单、均匀。缺点:需要事先知道关键字的分布情况。使用场景:适合查找比较小且连续的情况。

        除留余数法:设散列表中允许的地址数为m,取⼀个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

        哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

        我们还有另外一种就是调节负载因子。哈希表的载荷因子为:ą=填入表中元素个数/哈希表长度。当冲突率达到⼀个⽆法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。

1.4. 解决冲突

        解决哈希冲突两种常⻅的⽅法是:闭散列和开散列。

        闭散列:也叫开放地址法,当发⽣哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下⼀个” 空位置中去。那如何寻找下⼀个空位置呢?此时我们就需要应用线性探索。从发生冲突的位置开始,依次向后探测,直到寻找到下⼀个空位置为止。但这样还是会有一个缺点,就是会使得冲突元素聚集在一起,并且如果我们把4删除了,又如何寻找14、24、34这些元素。因此线性探测采⽤标记的伪删除法来删除⼀个元素。

        ⼆次探测为了避免该问题,找下⼀个空位置的⽅法为:Hi = (H0+i^2)%m,i表示冲突的次数,m为表的大小。

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

1.5. 实现

        由于我们需要节点数组来创建哈希表,利用内部类来表示节点对象。

public class HashBucket {
    //创建节点数组
    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 = new Node[10];
    public int UsedSize;//表示还系统中存放的元素
    public static final float LOAD_FACTOR = 0.75f;//负载因子表示为常数
}

        我们先来模拟哈希表中放元素的方法。我们要想把元素放入,首先得是一个结点。比如key=14,如果表中已经有14了,就不能再放14并且更新val,所以我们首先需要遍历数组判断key是否相同,如果相同,更新val。下面再使用头插法来把节点元素放入哈希表中。插入元素之后,我们还需要重新计算负载因子是否超过了我们规定的LOAD_FACTOR。如果超过了,就需要进行扩容操作。扩容的时候还需要注意,比如我们要插入的元素的key为14,扩容前需要插入下标为4的位置,扩容2倍后,就需要插入下标为14的位置。

        完整代码实现:

    public void put(int key, int val) {
        int index = key % array.length;

        //先遍历index数组下的链表,如果有相同的key,则更新val
        Node cur = array[index];
        while (cur != null) {
            if (cur.key == key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }

        //头插法
        Node node = new Node(key, val);
        node.next = array[index];
        array[index] = node;
        UsedSize++;

        //重新计算负载因子是不是超过了我们规定的值
        if (CalculateLoadFactor() >= LOAD_FACTOR) {
            //扩容
            ReSize();
        }
    }

    private float CalculateLoadFactor() {
        return UsedSize * 1.0f / array.length;
    }

    private void ReSize() {
        Node[] newArray = new Node[array.length*2];
        for (Node node : array) {
            Node cur = node;
            while (cur != null) {
                int newIndex = cur.key % newArray.length;
                //把当前节点放入新数组的位置,再次使用头插法
                Node curNext = cur.next;
                cur.next = newArray[newIndex];
                newArray[newIndex] = cur;
                cur = curNext;
            }
        }
        array = newArray;
    }

        get方法也是一样,也是通过索引下标来寻找目标值。

    public int get(int key){
        int index = key % array.length;
        Node cur = array[index];
        while(cur != null){
            if(cur.key == key){
                return cur.val;
            }
            cur = cur.next;
        }
        return -1;
    }

        我们在Test类里面进行实例化并调试。

public class Test {
    public static void main(String[] args) {
        HashBucket hashBucket = new HashBucket();
        hashBucket.put(11,99);
        hashBucket.put(2,99);
        hashBucket.put(43,99);
        hashBucket.put(4,99);
        hashBucket.put(14,99);
        hashBucket.put(24,99);
        hashBucket.put(7,99);
        hashBucket.put(8,99);
    }
}

        我们上面的方法key是整型,那如果key是引用类型呢,比如String或者Person类。那我们就把整型换作是泛型K、V。需要注意的是,key换成了泛型,不能直接进行%操作,我们可以使用hashCode方法转成整型,并且进行比较要使用equals方法。

/**
 * @author: gao
 * @create-date: 2025/3/15 16:32
 */

public class HashBucket<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[10];
    public int UsedSize;//表示还系统中存放的元素
    public static final float LOAD_FACTOR = 0.75f;//负载因子表示为常数

    public void put(K key, V val) {
        int hashcode = key.hashCode();
        int index = hashcode % array.length;

        //先遍历index数组下的链表,如果有相同的key,则更新val
        Node<K, V> cur = array[index];
        while (cur != null) {
            if (cur.key == key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        Node<K, V> node = new Node<K, V>(key, val);
        node.next = array[index];
        array[index] = node;
        UsedSize++;
    }

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

        如果我们要判断是否为同一个人,我们可以判断身份证号码是否相等。如果我们按照这种方法去写,发现比较结果为false。这是因为我们没有重写equals和hashCode方法,编译器默认调用Object方法。

class Person {
    public String id;

    public Person(String id) {
        this.id = id;
    }
}

public class Test {
    public static void main(String[] args) {
        Person person1 = new Person("1234");
        Person person2 = new Person("1234");
        System.out.println(person1.equals(person2));
        System.out.println(person1.hashCode());
        System.out.println(person2.hashCode());
    }
}

    public boolean equals(Object obj) {
        return (this == obj);
    }

        我们在Person类里面右击,点击Generate,再点击equals() and hashCode(),就可以重写。

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(id, person.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

二、OJ练习

2.1. 只出现一次的数字

        我们的基本思路是:利用HashSet,先遍历一遍数组,把集合中没有的数字放入,如果有,再移除,最后集合中剩下的元素就是只出现一次的数字,再遍历一遍数组,匹配HashSet中的数组。

        完整代码实现:

class Solution {
    public int singleNumber(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for (int i = 0;i < nums.length;i++) {
            if(! set.contains(nums[i])){
                set.add(nums[i]);
            }else{
                set.remove(nums[i]);
            }
        }
        for (int i = 0;i < nums.length;i++) {
            if(set.contains(nums[i])){
                return nums[i];
            }
        }
        return -1;
    }
}

        执行时间还是比较高,因为使用了两次for循环遍历数组。

2.2. 随机链表的复制

        题目比较长,大概题意就是复制出一份与原来相同的链表。这道题的难点在于比单链表多了一个可以指向任意节点或者空的random域。起初,很多人会去想定义一个Node cur去遍历一遍链表,一个一个节点进行拷贝,但一拷贝就会发现问题,因为我们我们不知道cur.next和cur.random是哪一个节点的地址。既然遍历一遍链表不行,那就遍历两遍。第一遍遍历,所有节点的val域全都拷贝过来,next域以及random域全都默认为null,每遍历一个链表,就新实例化一个节点。然后我们<K,V>结构来建立老节点与新节点之间的映射关系。

        我们每获取一个节点的地址,都可以修改它的next域与random域。

        完整代码实现:

class Solution {
    public Node copyRandomList(Node head) {
        Map<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);
    }
}

 2.3. 宝石与石头

        题目很简单,就是查找stones中含有jewels中的字符的个数。我们先遍历jewels字符串,将里面的字符放入集合中,再去遍历stones中的字符,最后返回宝石个数。

        完整代码实现:

class Solution {
    public int numJewelsInStones(String jewels, String stones) {
        Set<Character> set = new HashSet<>();
        for (int i = 0; i < jewels.length(); i++) {
            char ch = jewels.charAt(i);
            set.add(ch);
        }
        int count = 0;
        for (int i = 0; i < stones.length(); i++) {
            char ch = stones.charAt(i);
            if(set.contains(ch)){
                count++;
            }
        }
        return count;
    }
}

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

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

相关文章

linux系统命令——权限

一、有哪些权限 读&#xff08;r&#xff09;——对应数字4 写&#xff08;w&#xff09;——对应数字2 执行&#xff08;x&#xff09;——对应数字1 二、权限及数字的对应 4对应r-- 2对应-w- 1对应--x 5对应r-x 6对应rw- 7对应rwx 三、文件的基本属性 如图&#…

PentestGPT 下载

PentestGPT 下载 PentestGPT 介绍 PentestGPT&#xff08;Penetration Testing GPT&#xff09;是一个基于大语言模型&#xff08;LLM&#xff09;的智能渗透测试助手。它结合了 ChatGPT&#xff08;或其他 GPT 模型&#xff09;与渗透测试工具&#xff0c;帮助安全研究人员自…

JVM 2015/3/15

定义&#xff1a;Java Virtual Machine -java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&#xff0c;到处运行 自动内存管理&#xff0c;垃圾回收 数组下标越界检测 多态 比较&#xff1a;jvm/jre/jdk 常见的JVM&…

sql靶场-时间盲注(第九、十关)保姆级教程

目录 时间盲注&#xff08;第九、十关&#xff09; 1.判断 2.确认时间盲注 2.手工尝试时间盲注 数据库名长度 数据库名字符 表数 表名长度 表名字符 字段数 字段名长度 字段名字符 4.脚本时间盲注注入 5.第十关 时间盲注&#xff08;第九、十关&#xff09; 1.判…

51c自动驾驶~合集54

我自己的原文哦~ https://blog.51cto.com/whaosoft/13517811 #Chameleon 快慢双系统&#xff01;清华&博世最新&#xff1a;无需训练即可解决复杂道路拓扑 在自动驾驶技术中&#xff0c;车道拓扑提取是实现无地图导航的核心任务之一。它要求系统不仅能检测出车道和交…

大模型推理:LM Studio在Mac上部署Deepseek-R1模型

LM Studio LM Studio是一款支持离线大模型部署的推理服务框架&#xff0c;提供了易用的大模型部署web框架&#xff0c;支持Linux、Mac、Windows等平台&#xff0c;并提供了OpenAI兼容的SDK接口&#xff0c;主要使用LLama.cpp和MLX推理后端&#xff0c;在Mac上部署时选择MLX推理…

扩散模型:AIGC领域的核心引擎,解锁图像生成新维度

一、扩散模型技术原理 扩散模型是一类生成模型&#xff0c;它运用了物理热力学中的扩散思想&#xff0c; 主要包括前向扩散和反向扩散两个过程。 1.1、生成模型 在深度学习中&#xff0c;生成模型的目标是根据给定的样本&#xff08;训练数据&#xff09; 生成新样本。首先给…

Java多线程与高并发专题——原子类和 volatile、synchronized 有什么异同?

原子类和 volatile异同 首先&#xff0c;通过我们对原子类和的了解&#xff0c;原子类和volatile 都能保证多线程环境下的数据可见性。在多线程程序中&#xff0c;每个线程都有自己的工作内存&#xff0c;当多个线程访问共享变量时&#xff0c;可能会出现一个线程修改了共享变…

【数据结构】数据结构,算法 概念

0.本篇问题&#xff1a; 数据、数据元素、数据对象、数据项之间的基本关系&#xff1f;ADT是什么&#xff1f;数据结构的三要素&#xff1f;数据的逻辑结构有哪些&#xff1f;数据的存储结构有哪些&#xff1f;算法的五个特征&#xff1f;O(1) O(logn) O(n^n) O(n) O(n^2…

总结 HTTP 协议的基本格式, 相关知识以及抓包工具fiddler的使用

目录 1 HTTP是什么 2 HTTP协议格式 3 HTTP请求(Request) 3.1 认识URL 3.2 方法 3.3 认识请求"报头"(header) 4 HTTP响应详解 4.1 认识"状态码"(statuscode) 4.2 认识响应"报头"(header) 4.3 认识响应"正⽂"(body) 5 通过f…

探索Maas平台与阿里 QWQ 技术:AI调参的魔法世界

摘要&#xff1a;本文介绍了蓝耘 Maas 平台在人工智能领域的表现及其核心优势&#xff0c;包括强大的模型支持、高效的资源调度和友好的操作界面。文章还探讨了蓝耘 Maas 平台与阿里 QWQ 技术的融合亮点及应用拓展实例&#xff0c;并提供了调参实战指南&#xff0c;最后对蓝耘 …

Linux第三次练习

1、创建根目录结构中的所有的普通文件 首先在根目录下面新创建一个test目录&#xff0c;然后将查找到的普通文件新建到test目录下 2、列出所有账号的账号名 3、将/etc/passwd中内容按照冒号隔开的第三个字符从大到小排序后输出所有内容 4、列出/etc/passwd中的第20行-25行内容…

软件测试知识总结

1、黑盒测试、白盒测试、灰盒测试 1.1 黑盒测试 黑盒测试又叫功能测试、数据驱动测试 或 基于需求规格说明书的功能测试。该类测试注重于测试软件的功能性需求。 采用这种测试方法&#xff0c;测试工程师把测试对象看作一个黑盒子&#xff0c;完全不考虑程序内部的逻辑结构和…

【HTML】三、表单与布局标签

文章目录 1、input1.1 input的占位文案1.2 单选框1.3 上传文件1.4 多选框 2、 下拉菜单3、文本域&#xff1a;多行输入4、label标签&#xff1a;说明与增大点击范围5、按钮与form表单6、无语义布局标签7、有语义的布局标签8、字符实体9、练习&#xff1a;注册页面 1、input in…

【结构设计】3D打印创想三维Ender 3 v2

【结构设计】3D打印创想三维Ender 3 v2 文章目录 前言一、Creality Slicer1.2.3打印参数设置二、配件更换1.捆扎绑扎线2.气动接头3D打印机配件插头3.3D打印机配件Ender3pro/V2喷头套件4.读卡器 TF卡5.micro sd卡 三、调平四、参考文章总结 前言 使用工具&#xff1a; 1.创想三…

UE小:UE5.5 PixelStreamingInfrastructure 使用时注意事项

1、鼠标默认显示 player.ts中的Config中添加HoveringMouse:true 然后运行typescript\package.json中的"build":npx webpack --config webpack.prod.js

Anaconda 入门指南

Anaconda 入门指南 一、下载安装 Anaconda 1、下载地址&#xff1a;Anaconda 推荐下载 python3 版本, 毕竟未来 python2 是要停止维护的。 2、安装 Anaconda 按照安装程序提示一步步安装就好了, 安装完成之后会多几个应用&#xff1a; Anaconda Navigtor &#xff1a;用于管…

web组态可视化编辑器

Web组态可视化编辑器是一种用于创建和配置工业自动化、物联网&#xff08;IoT&#xff09;和智能建筑等领域的图形化用户界面&#xff08;GUI&#xff09;的工具。它允许用户通过拖放组件、配置参数和连接数据源来设计和部署实时监控和控制界面。以下是一些常见的Web组态可视化…

CTA重建:脑血管重建,CT三维重建,三维建模 技术,实现

CTA&#xff08;CT血管造影&#xff09;是一种基于CT扫描的医学成像技术&#xff0c;主要用于血管系统的三维重建和可视化。脑血管重建是CTA的重要应用之一&#xff0c;能够帮助医生诊断脑血管疾病&#xff08;如动脉瘤、狭窄、畸形等&#xff09;。以下是实现CTA脑血管重建、C…

Ollama+OpenWebUI本地部署大模型

OllamaOpenWebUI本地部署大模型 前言Ollama使用Ollama安装Ollama修改配置Ollama 拉取远程大模型Ollama 构建本地大模型Ollama 运行本地模型&#xff1a;命令行交互Api调用Web 端调用 总结 前言 Ollama是一个开源项目&#xff0c;用于在本地计算机上运行大型语言模型&#xff0…