数据结构与算法【哈希表】的Java实现

news2024/9/23 21:32:47

目录

介绍

实现哈希表

大体框架

实现数组扩容

实现查询key

实现新增元素

实现删除元素

哈希算法

String中重写的hashCode()方法


介绍

哈希表也叫散列表,哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1)。在实现哈希表时,如果只靠数组存储,当需要存储大量元素时,系统很难在内存中找到连续的内存空间。因此需要结合链表来存储大量数据,当链表长度过高时,会转化成红黑树。其次哈希码是可以重复的,当重复时根据数据的值来进行区分。接下来以代码的形式来解释

实现哈希表

大体框架

hash码我们暂时自己设置,hash码与数组索引的关系采用取模运算,为了提高效率,我们采用位运算(不熟悉的可以查看另一篇文章),数组长度需要设置为 2^n,如果数组长度不满足,那么后续的实现代码会存在错误。

public class HashTable {
    //定义一个数组
    Entry[] table = new Entry[16];
    int size = 0; // 元素个数
    float loadFactor = 0.75f;//当(size/数组长度) = 0.75时进行扩容
    int threshold = (int) (loadFactor * table.length);// 阈值计算

    static class Entry{
        int hash; // 哈希码
        Object key; // 键
        Object value; // 值
        Entry next;

        public Entry(int hash, Object key, Object value) {
            this.hash = hash;
            this.key = key;
            this.value = value;
        }
    }
}

实现数组扩容

首先我们需要了解什么时候需要进行扩容,以及扩容后原来元素应该如何处理。

当表中存储的元素到达阈值后,需要进行扩容,而扩容的元素的存储位置也需要发生改变。首先就是对链表拆分,一个旧链表最多拆分成两条新链表,然后分别存储在新数组中的不同位置。拆分规律为,hash码对原数组长度进行与运算后等于 0 的节点拆分成一条链表,不等于 0 的节点拆分成另一条链表。

比如说,当原数组中存在一条hash码为0->8->16->24->32->40-> null 的链表。该链表中的每一个hash值都需要对原数组的长度进行与运行。运算结果如下

0 & 8 = 0

8 & 8 = 8

16 & 8 = 0

24 & 8 = 8

32 & 8 = 0

40 & 8 = 8

因此接下来需要将该链表拆分为如下两条链表。

0->16->32->null

8->24->40->null

拆分后,需要将两条新的链表分别存储在新数组中的原始索引下标位置与原始索引下标+原始数组长度的位置。

具体实现代码如下

private void resize() {
    //创建出新数组,并且扩容一倍
    Entry[] newTable = new Entry[table.length << 1];
    for (int i = 0; i < table.length; i++) {
        //拿到每个位置的头元素
        Entry head = table[i];
        if (head != null) {
            //讲链表进行拆分
            //记录a链表的头位置
            Entry aHead = null;
            //记录b链表的头位置
            Entry bHead = null;
            Entry a = null;
            Entry b = null;
            while (head != null) {
                if ((head.hash & table.length) == 0) {
                    if (a != null) {
                        //如果a!=null,那么a的下一个元素设置为head
                        a.next = head;
                    } else {
                        //如果a为null,说明还没有拆分出a链表
                        aHead = head;
                    }
                    //a指针后移
                    a = head;
                } else {
                    if (b != null) {
                        b.next = head;
                    } else {
                        bHead = head;
                    }
                    b = head;
                }
                head = head.next;
            }
            //处理拆分链表后结尾指针指向位置
            if (a != null) {
                a.next = null;
                newTable[i] = aHead;
            }
            if (b != null) {
                b.next = null;
                //拆分后,a链表仍插入新数组中的原始位置,另一个链表需要插入原始位置加原数组长度的下标位置
                newTable[i + table.length] = bHead;
            }
        }
    }
    table = newTable;
    //更新新的阈值
    threshold = (int) loadFactor*table.length;
}

实现查询key

在JDK中的哈希表中,查询元素的参数中并没有hash,但是我们选择了手动提供,因此需要添加该参数。

具体实现代码如下

public Object get(int hash,Object key){
    //根据hash码获取在数组中的下标
    int index = hash & (table.length - 1);
    //根据索引获取到保存在数组中的链表头元素
    Entry entry = table[index];
    if (entry ==null){
        return null;
    }
    //遍历链表,获取key的值
    while(entry!=null){
        if (entry.key.equals(key)){
            return entry.value;
        }
        entry = entry.next;
    }
    return null;
}

实现新增元素

如果指定key在哈希表中不存在则新增,如果存在则更新。

具体实现代码如下

public void put(int hash, Object key, Object value) {
    int index = hash & (table.length - 1);
    Entry entry = table[index];
    //如果该位置还没有元素
    if (entry == null) {
        table[index] = new Entry(hash, key, value);
        return;
    }

    while (true) {
        //如果找到了则更新
        if (entry.key.equals(key)) {
            entry.value = value;
            return;
        }
        if (entry.next == null) {
            //如果没找到,添加在链表尾部
            break;
        }
        entry = entry.next;
    }
    entry.next = new Entry(hash, key, value);
    size++;
    if (size > threshold){
        resize();
    }
}

实现删除元素

public Object remove(int hash, Object key) {
    int index = hash & (table.length - 1);
    Entry entry = table[index];
    if (entry == null) {
        return null;
    }
    Entry p = entry;
    Entry prev = null;
    while (p != null) {
        if (p.key.equals(key)) {
            if (prev == null) {
                table[index] = p.next;
            } else {
                prev.next = p.next;
            }
            size--;
            return p.value;
        }
        prev = p;
        p = p.next;
    }
    return null;
}

哈希算法

所谓哈希算法就是将任意对象分配一个编号的过程,其中编号是一个有范围限制的数组。而常见的哈希算法有MD5、SHA1、SHA256等。常用的哈希算法是MD5与SHA系列的算法。

最简单的获取hash码的方法是调用JDK中的hashCode()方法。不同对象的hash值可能会相同,但是同一对象的hash值一定相同

String中重写的hashCode()方法

如果是不同对象的hash码不同,那么如下代码中s1与s2的哈希码应该是不同的

public static void main(String[] args) {
    String s1 = "abc";
    String s2 = new String("abc");
    System.out.println(s1 == s2);
    System.out.println(s1.hashCode());
    System.out.println(s2.hashCode());
}

但是运行的输出结果为

false

96354

96354

哈希码是一样的,因此我们可以知道String类中,重写了hashCode()方法。具体重写方法如下

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

String中重写的方法中,首先需要将每个对象中的每个value属性(也就是字符串)转换成char数组,然后对数组中的不同位数的字符转化成ASCII码后,乘以一个权重值31(为了避免hash冲突,因此乘以一个质数)后相加。

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

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

相关文章

Flink 常用物理分区算子(Physical Partitioning)

Flink 物理分区算子(Physical Partitioning) 在Flink中&#xff0c;常见的物理分区策略有&#xff1a;随机分配(Random)、轮询分配(Round-Robin)、重缩放(Rescale)和广播(Broadcast)。 接下来&#xff0c;我们通过源码和Demo分别了解每种物理分区算子的作用和区别。 (1) 随机…

数组题目: 665. 非递减数列、453. 最小移动次数使数组元素相等、283. 移动零、189. 旋转数组、396. 旋转函数

665. 非递减数列 题解&#xff1a; 题目要求一个非递减数列&#xff0c;我们可以考虑需要更改的情况&#xff1a; nums {4, 2, 5} 对于这个nums&#xff0c;由于2的出现导致非递减&#xff0c;更改的情况就是要么4调到<2&#xff0c;要么2调到4,5. nums {1, 4, 2, 5} …

从Redis反序列化UserDetails对象异常后发现FastJson序列化的一些问题

最近在使用SpringSecurityJWT实现认证授权的时候&#xff0c;出现Redis在反序列化userDetails的异常。通过实践发现&#xff0c;使用不同的序列化方法和不同的fastJson版本&#xff0c;异常信息各不相同。所以特地记录了下来。 一、项目代码 先来看看我项目中redis相关配置信息…

为什么说巴罗洛是意大利葡萄酒中的极品?

在来自南欧国家的众多优秀葡萄酒中&#xff0c;巴罗洛是最好最著名的意大利红酒之一。巴罗洛是一种来自意大利的高品质红酒&#xff0c;巴罗洛红酒是干的&#xff0c;浓郁的&#xff0c;富含单宁和酒精&#xff0c;典型的水果和泥土的味道。巴罗洛产区位于该国北部的皮埃蒙特地…

x-www-form-urlencoded的含义解释,getReader()和getParameter()的区别

1、x-www-form-urlencoded x-www-form-urlencoded是一种编码格式&#xff0c;它是一种常见的编码方式&#xff0c;用于在HTTP请求中 传输表单数据 。在这种编码方式下&#xff0c;表单数据被编码为URL格式&#xff0c;然后作为请求体&#xff08;payload&#xff09;发送。 需要…

Langchain的Agents介绍

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

Ardupilot开源飞控之VTOL之旅:开箱

Ardupilot开源飞控之VTOL之旅&#xff1a;开箱 1. 源由2. 收货2.1 外包装2.2 内包装2.3 部件2.3 概貌 3. 探索3.1 飞控VTOL3.2 远程控制3.3 自动导航3.4 部件清单 4. 计划 1. 源由 心系已久的HEE WING T1 Ranger VTOL终于来了&#xff0c;因此开启了VTOL之旅。 当然Ardupilot…

jenkins + gitlab 自动部署(webhook)

Jenkins是一个流行的开源CI/CD工具&#xff0c;可以与Git等版本控制系统集成&#xff0c;实现自动构建、测试和部署。Webhook是一种机制&#xff0c;可以在Git仓库中设置&#xff0c;在代码提交或合并请求时触发Jenkins构建任务&#xff0c;以完成自动化部署。 实操 设备信息 …

计算机中mfc140u.dll丢失的修复方法,3个完美解决的方法分享

在使用电脑的过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“mfc140u.dll丢失”。这个错误提示通常出现在运行某些程序时&#xff0c;它会导致程序无法正常运行。那么&#xff0c;究竟是什么原因导致了mfc140u.dll文件的丢失呢&#xff1f;本文将详细…

语雀服务器P0事故的一些启发

文章目录 背景错误显示故障原因及处理过程改进措施补偿启发监控和告警容灾备份自动化部署和回滚灰度发布定期演练和测试日志和审计容错性弹性扩展性能优化安全性持续改进稳定业务不动多方验证不要抱着侥幸心理白名单内测留后手总结 写在最后 背景 语雀是蚂蚁金服旗下的一款在线…

C++算法 —— 贪心(4)

文章目录 1、分发饼干2、最优除法3、跳跃游戏Ⅱ4、跳跃游戏Ⅰ5、加油站6、单调递增的数字7、坏了的计算器 1、分发饼干 455. 分发饼干 其实看完这个题会发现&#xff0c;如果给定的两个数组不排序的话会非常难受&#xff0c;所以无论怎样&#xff0c;先排序。接下来需要比较两…

蓝桥杯每日一题2023.11.24

题目描述 #include <stdio.h> #define N 100int connected(int* m, int p, int q) {return m[p]m[q]? 1 : 0; }void link(int* m, int p, int q) {int i;if(connected(m,p,q)) return;int pID m[p];int qID m[q];for(i0; i<N; i) ________________________________…

软文写作如何布局?媒介盒子分享三大类型

好的软文需要有清晰的结构和流畅的语言&#xff0c;让读者能够很快理解和接受文案的内容&#xff0c;因此在写文案之前&#xff0c;需要先列出思路和框架&#xff0c;明确文案的主题和重点&#xff0c;选择合适的语言和表达方式。让文案更加生动易懂&#xff0c;下面就让媒介盒…

yo!这里是c++11重点新增特性介绍

目录 前言 列表初始化 { }初始化 initializer_list类 类型推导 auto decltype 范围for 右值引用与移动语义 左值引用和右值引用 移动语义 1.移动构造 2.移动赋值 3.stl容器相关更新 右值引用和万能引用 完美转发 关键字 default delete final和override …

数组基础知识

数组基础&#xff08;不定时更新&#xff09; 数组基础 数组基础 &#xff08;1&#xff09;数组是存放在连续内存空间上的相同类型数据的集合。数组可以方便的通过下标索引的方式获取到下标下对应的数据。数组下标都是从0开始的。数组内存空间的地址是连续的。 &#xff08;…

python-选择排序

选择排序是一种简单直观的排序算法&#xff0c;它的基本思想是每一轮选择未排序部分的最小元素&#xff0c;然后将其放到已排序部分的末尾。这个过程持续进行&#xff0c;直到整个数组排序完成。(重点&#xff1a;通过位置找元素) 以下是选择排序的详细步骤和 Python 实现&…

element ui 上传组件实现手动上传

首先需要给上传组件增加http-request属性&#xff0c;这个方法中可以获取到文件&#xff0c;并按照自己的方式进行上传。 <el-uploadreffileUploadaction#:http-requesthttpRequest:on-preview"handlePreview":on-remove"handleRemove":limit"1&q…

SpringBoot3核心原理

SpringBoot3核心原理 事件和监听器 生命周期监听 场景&#xff1a;监听应用的生命周期 可以通过下面步骤自定义SpringApplicationRunListener来监听事件。 ①、编写SpringApplicationRunListener实现类 ②、在META-INF/spring.factories中配置org.springframework.boot.Sprin…

接口测试:轻松掌握基础知识,快速提升测试技能!

1.client端和server端 开始接口测试之前&#xff0c;首先搞清楚client端与server端是什么&#xff0c;区别。 web前端&#xff0c;顾名思义&#xff0c;指用户可以直观操作和看到的界面&#xff0c;包括web页面的结构&#xff0c;web的外观视觉表现及web层面的交互实现。 web后…

Python---函数的参数类型

位置参数 理论上&#xff0c;在函数定义时&#xff0c;我们可以为其定义多个参数。但是在函数调用时&#xff0c;我们也应该传递多个参数&#xff0c;正常情况&#xff0c;其要一一对应。 相关链接&#xff1a;Python---函数的作用&#xff0c;定义&#xff0c;使用步骤&…