Java实现哈希表

news2024/12/23 12:52:47

1.哈希表定义

        哈希表(hash table,也叫散列表),是根据关键码值(key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫散列函数,存放记录的数组叫散列表。

哈希表可以提供快速的插入和查找工作,哈希表运算的非常快,而且编程实现也比较容易。哈希表是数组和链表结构。 

2.哈希表的原理 

        1.哈希表是链表和数组实现的(数组里面存储的元素是链表的头结点)

 

        2.哈希表是环形数组实现的

        什么是环形数组呢?

就是近似于环,数组的最大索引后面就是0索引,这种思想主要是通过取模计算实现的,

主要公式:  index = index % arr.length

就按照上图举例吧: 例如 计算出hash值为 10   10 % 8 = 2 所以应该存放 2 索引的位置

        3.为什么Jdk会在链表长度超过8时转换为红黑树?

正常数据情况下,就算数据量比较大,也不会出现超过8的情况!

但是为什么会用这种机制呢?

这种机制呢,主要是为了防范于未然,防止被攻击,有些人专门造一些攻击的Hash数据(这些hash值都会相互之间冲突),就会形成一个非常长的链表,会使你整个服务器下降,

主要目的:为了防止被恶意攻击

        4.为什么Jdk的底层数组长度都是2^n?

.位运算符(&)比模运算(%),效率高,这样做可以提高效率。

        它对应的二进制表示中只有一位是1,其余位都是0。在这种情况下,计算哈希值h与哈希表长度m的取模运算等价于对2^n取模,可以使用位运算(&)实现,即 h & (m - 1)。

所以长度为2 ^ n就会有一个规律:   [hash & (数组长度-1)] 等价于 [hash % 数组长度]

这样的做法的好处是:当哈希表长度是2^n时,可以用位运算来代替较慢的取模运算,从而提高哈希表的性能。并且由于2^n的二进制表示中只有一位是1,因此在使用位运算计算哈希表索引时,可以保证结果均匀分布,减少哈希碰撞的概率。

需要注意的是,即使哈希表的长度不是2^n,仍然可以使用位运算来代替取模运算。但是,这样做会使得位运算的效率降低,并且不能保证哈希值均匀分布,容易导致哈希冲突,影响哈希表的性能。

 3.代码原理分析

3.1添加

1.hash表在添加的使用key.hashCode()计算出的键的hash值

2.再利用位运算符&数组长度 - 1(% 数组长度)计算出应该要存的下标

3.接下俩就是数组中有元素吗? 

        3.1 没有则直接存

        3.2 有的话,一直往后找,找到最后一个元素,添加在它的尾部

3.2删除

删除的原理:主要思想就是找到删除元素上一个节点(如果是头部,则头部下一个节点为空)

1.根据要删除的元素,计算其hash值,并找到对应的数组索引。

2.在该索引位置查找元素,如果存在,则执行删除操作。

3.删除元素后,根据具体情况可能需要进行以下操作:

        3.1 如果删除后该位置没有其他元素,则直接将该位置设为null,表示该位置为空。

        3.2如果删除后该位置有其他元素(可能是发生了哈希冲突),则可能需要进行链表或其他数据结构的调整,以保持哈希表的正确性。

具体删除时的操作流程可能因不同的哈希表实现而有所不同。例如,对于开放寻址法的哈希表实现,在删除元素时可能会使用查找下一个空槽的方式来处理哈希冲突。而对于拉链法的哈希表实现,在删除元素时可能需要遍历链表结构并进行节点的删除操作。

需要注意的是,哈希表的删除操作可能会导致哈希表的负载因子过低或链表过长等问题,影响哈希表的性能。为了保持哈希表的高效性,可能需要根据具体情况进行动态缩容、重新哈希或其他优化操作。

3.3扩容

哈希表的扩容是为了保持哈希表的负载因子,在一个可接受的范围内,从而保持哈希表的性能稳定。

当哈希表中的元素数量达到一定阈值(0.75)时,就会触发扩容操作。具体的扩容原理如下:

1.创建一个新的更大的数组,通常将数组长度扩大为原来的两倍。

2.遍历原来的哈希表,将每个元素重新计算哈希值,并放入新的数组中的对应位置

3.这个过程被称为重新哈希(Rehashing),它会根据新数组大小重新计算元素的存储位置,以确保元素在新数组中的分布更为均匀,减少哈希冲突。

4.将新的数组设置为哈希表的底层数组,用于存储元素。

4.代码实现

注意:这里我怕不好理解我没有用位运算符(&)全部用了%

4.1准备工作

定义节点类,和定义成员变量

// 哈希表的代码实现
public class HashTable<K,V>{
    // 定义哈希表节点
    class HashNode<K, V>{
        private final K key;//键
        private V value;//值
        private HashNode<K, V> next;//下一个节点的引用


        public HashNode(K key, V value) {
            this.key = key;
            this.value = value;
            this.next = null;
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }

        public HashNode<K, V> getNext() {
            return next;
        }

        public void setNext(HashNode<K, V> next) {
            this.next = next;
        }
    }

    private int SIZE;//定义数组的长度

    private final double LOAD_FACTOR = 0.75; // 负载因子,用于决定何时进行扩容

    private int size; // 哈希表中节点的数量

    private HashNode<K,V>[] table;//定义一个存链表的数组

    public HashTable(int initialCapacity) {//初始化数组
        this.SIZE = initialCapacity;
        table = new HashNode[SIZE];

        this.size = 0;//初始化节点数量为0
    }
  }

4.2计算键的应该存入的下标

 private int getHash(K key) {
        //利用jdk的哈希算法来产生一个随机数
        return Math.abs(key.hashCode() % SIZE);//获得键的hash值应放入的索引,取绝对值以保证正数
    }

4.3扩容

//扩容
    private void resize() {
        int newSize = SIZE * 2; // 新的哈希表大小为原来的两倍
        HashNode<K, V>[] newTable = new HashNode[newSize]; // 创建新的数组

        for (int i = 0; i < SIZE; i++) {
            HashNode<K, V> current = table[i]; // 获取原数组的每个链表的头节点
            while (current != null) {
                K key = current.getKey();
                V value = current.getValue();

                // 根据新的数组大小重新计算哈希值
                int hash = Math.abs(key.hashCode() % newSize);

                // 插入节点到新的数组中
                if (newTable[hash] == null) {//头结点
                    newTable[hash] = new HashNode<>(key, value);
                } else {//非头结点
                    HashNode<K, V> newNode = newTable[hash];
                    while (newNode.getNext() != null) {//找到next不为null的节点进行存储
                        newNode = newNode.getNext();
                    }
                    newNode.setNext(new HashNode<>(key, value));
                }

                current = current.getNext(); // 继续遍历原数组的下一个节点
            }
        }

        table = newTable; // 将引用指向新的数组
        SIZE = newSize; // 更新数组大小
    }

4.4添加

public void put(K key,V value){//向hash表中插入键值对
        int hash = getHash(key);//计算键的hash值放入的索引

        if (table[hash] == null){ //为空
            table[hash] = new HashNode<>(key,value);//直接插入节点
        }else {//不为空
            HashNode<K, V> current = table[hash];
            while (current.getNext() != null) { // 遍历链表,直到找到最后一个节点
                if (current.getKey().equals(key)) { // 如果找到键相同的节点
                    current.setValue(value); // 更新值
                    return;
                }
                current = current.next;
            }
            //找不到就判断最后一个是不是
            if (current.getKey().equals(key)) { // 如果最后一个节点的键与目标键相同
                current.setValue(value); // 更新值
            } else { // 如果最后一个节点的键与目标键不同
                current.setNext(new HashNode<>(key, value)); // 插入新节点作为最后一个节点的下一个节点
            }
        }
        // 检查负载因子是否超过阈值,如果超过则进行扩容
        if ((double) size / SIZE > LOAD_FACTOR) {
            resize();
        }


    }

4.5删除

public void remove(K key) { // 移除指定键的节点
        int hash = getHash(key); // 计算键的哈希值
        HashNode<K, V> previous = null; // 记录前一个节点
        HashNode<K, V> current = table[hash]; // 获取对应索引位置的节点
        while (current != null) { // 遍历链表
            if (current.getKey().equals(key)) { // 如果找到键相同的节点
                if (previous == null) { // 如果当前节点为链表的第一个节点
                    table[hash] = current.getNext(); // 将下一个节点设为新的头节点
                } else { // 如果当前节点不是链表的第一个节点
                    previous.setNext(current.getNext()); // 将上一个节点的next指向当前节点的下一个节点,跳过当前节点
                }
                return;
            }
            previous = current; // 更新前一个节点
            current = current.getNext(); // 继续遍历下一个节点
        }
    }

4.5判空

public boolean isEmpty() { // 判断哈希表是否为空
        for (int i = 0; i < SIZE; i++) { // 遍历数组
            if (table[i] != null) { // 如果存在非空的节点
                return false; // 表示哈希表不为空
            }
        }
        return true; // 如果数组中所有元素都为空,则返回true表示哈希表为空
    }

 

4.6获得键对应的值

 public V get(K key) { // 根据键获取值
        int hash = getHash(key);
        HashNode<K,V> current = table[hash];//获取对应索引的位置
        while (current != null) { // 遍历链表
            if (current.getKey().equals(key)) { // 如果找到键相同的节点
                return current.getValue(); // 返回对应的值
            }
            current = current.getNext(); // 继续遍历下一个节点
        }
        return null; // 如果未找到对应的键,则返回null
    }

4.7获得节点的个数

   public int size() { // 获取哈希表中节点的数量
        int count = 0; // 计数器
        for (int i = 0; i < SIZE; i++) { // 遍历数组
            HashNode<K, V> current = table[i]; // 获取对应索引位置的节点
            while (current != null) { // 遍历链表
                count++; // 计数器加一
                current = current.getNext(); // 继续遍历下一个节点
            }
        }
        return count; // 返回计数器的值,即哈希表中节点的数量
    }

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

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

相关文章

【深度学习实验】卷积神经网络(七):实现深度残差神经网络ResNet

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. Residual&#xff08;残差连接&#xff09; __init__&#xff08;初始化&#xff09; forward&#xff08;前向传播&#xff09; 2. resnet_block&#xff08;残…

9+代谢+分型,基于代谢通路对肝癌进行分型从而开展实验。

今天给同学们分享一篇代谢分型的生信文章“Bulk and single-cell transcriptome profiling reveal extracellular matrix mechanical regulation of lipid metabolism reprograming through YAP/TEAD4/ACADL axis in hepatocellular carcinoma”&#xff0c;这篇文章于2023年04…

【Linux 下 MySQL5.7 中文编码设置】

前言 原本要使用 Sqoop 把我 MySQL 的数据导入到 HBase 中&#xff0c;习惯了使用 windows 下的 MySQL 8.0 版本&#xff0c;但是用 Sqoop 从windows 传到 linux 下有点复杂&#xff0c;就索性用我自己之前没用过的 linux 下的 MySQL 5.7&#xff0c;结果果然一堆问题&#xff…

爱国者的润学日记-十月

首先需要科学的准备面试和润。如何进行科学的准备工作呢&#xff1f; 高效的按照面试考察内容进行针对性训练&#xff0c;按 Machine-learning-interview准备保证处于专注的心态&#xff0c;如今互联网娱乐发达&#xff0c;之前即使比赛时我也是一边比赛一边看视频。之后准备面…

MySQL:读写分离-amoeba(7)

环境介绍 mysql主服务器 192.168.254.1 mysql从服务器&#xff08;1&#xff09;192.168.254.2 mysql从服务器&#xff08;2&#xff09;192.168.254.3 amoeba代理服务器 192.168.254.4 测试服务器 192.168.254.5 此技术搭配主从复制&#xff0c;我的主服务器和从服务器都…

TS类中属性的封装

我们在如下的代码中&#xff0c;我们在类中设置属性&#xff0c;创建的对象可以随意修改自身的属性&#xff0c;对象中的属性可以任意被修改导致对象中的数据非常不安全。 // 创建一个Person类 class Person {name: string;age: number;constructor(name: string, age: number…

通道剪枝channel pruning

1、相关定义 过参数化&#xff1a;主要是指在训练阶段&#xff0c;在数学上需要进行大量的微分求解&#xff0c;去捕捉数据中微小的变化信息&#xff0c;一旦完成迭代式的训练之后&#xff0c;网络模型在推理的时候就不需要这么多参数。剪枝算法&#xff1a;核心思想就是减少网…

【【萌新的SOC学习之小水文系列】】

萌新的SOC学习之小水文系列 SD卡读写TXT文本实验 SD 卡共有 9 个引脚线&#xff0c;可工作在 SDIO 模式或者 SPI 模式。在 SDIO 模式下&#xff0c;共用到 CLK、CMD、DAT[3:0]六根信号线&#xff1b;在 SPI 模式下&#xff0c;共用到 CS&#xff08;SDIO_DAT[3]&#xff09;、…

栅形状的影响及可靠性的优化

栅形状的影响 VD-MOSFET单元结构采用平面栅极拓扑结构&#xff0c;栅极电极位于半导体的平坦上表面。虽然在这种结构中&#xff0c;在平面结处会发生电场增强&#xff0c;但在栅极电极处不会发生电场增强&#xff0c;因为栅极电极的边缘与高度掺杂的N源区重叠。栅极电极的边缘被…

新能源+低代码:百数服务商新领域,跨行业结合所碰撞出的新火花

新能源行业的兴起主要是在最近几年&#xff0c;特别是“双碳”目标提出后&#xff0c;中国的新能源行业迎来了快速发展的阶段。在政策支持和资本加持下&#xff0c;各种新能源和绿色发展基金设立&#xff0c;以新能源为主体的新型电力系统也得到了深化改革&#xff0c;大力推动…

Qt中QTimer定时器的用法

Qt中提供了两种定时器的方式一种是使用Qt中的事件处理函数&#xff0c;另一种就是Qt中的定时器类QTimer。 使用QTimer类&#xff0c;需要创建一个QTimer类对象&#xff0c;然后调用其start()方法开启定时器&#xff0c;此后QTimer对象就会周期性的发出timeout()信号。 1.QTimer…

十五、异常(6)

本章概要 Try-With-Resources 用法 揭示细节 异常匹配 Try-With-Resources 用法 在考虑所有可能失败的方法时&#xff0c;找出放置所有 try-catch-finally 块的位置变得令人生畏。确保没有任何故障路径&#xff0c;使系统远离不稳定状态&#xff0c;这非常具有挑战性。 Inp…

Unity ToLua热更框架使用教程(1)

从本篇开始将为大家讲解ToLua在unity当中的使用教程。 Tolua的框架叫LuaFramework&#xff0c;首先附上下载链接&#xff1a; https://github.com/jarjin/LuaFramework_UGUI_V2 这个地址的是UGUI的。 下载完之后导入项目&#xff0c;首先&#xff0c;我们要先让这个项目跑起…

老卫带你学---Datagrip连接clickhouse

Datagrip连接clickhouse Datagrip是一个DB可视化特别方便的软件&#xff0c;因为一些业务需要采用clickhouse&#xff0c;然而在download相关driver的时候出现各种问题&#xff0c;于是整理一下方案 1.需要下载clickhouse-jdbc的jar包&#xff0c;可以直接在sonatype上去下载…

C# 人像卡通化

效果 项目 代码 using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms;nam…

图像分割-Segment Anything实践

一、模型介绍 Segment Anything 模型是一种新的图像分割模型&#xff0c;它可以在不需要大量标注数据的情况下&#xff0c;对图像中的任何物体进行分割。这种方法可以帮助计算机视觉领域的研究人员和开发人员更轻松地训练模型&#xff0c;从而提高计算机视觉应用程序的性能。该…

超前预告 | 云原生?大模型?这届乌镇双态IT大会亮点有点多

石道旁的水面&#xff0c;轻轻泛着微光&#xff0c;几片墨绿缓缓飘下&#xff0c;荡起柔和的波纹&#xff0c;向对岸游去。这儿不似北方秋阳如火的躁动&#xff0c;这儿的秋色是安静的&#xff0c;里便是江南水乡乌镇…… 2023年&#xff0c;第六届双态IT乌镇用户大会将于10月…

不再为文件名大小写烦恼:批量转换,一招搞定

在电脑使用过程中&#xff0c;我们经常需要处理各种文件&#xff0c;有时需要对文件名进行大小写转换以符合特定要求或便于管理。手动修改不仅费时还容易出错&#xff0c;那么有没有一种方法可以批量转换文件名大小写呢&#xff1f;答案是肯定的&#xff0c;下面就为大家介绍如…

DC电源模块在电容滤波器上的设计

BOSHIDA DC电源模块在电容滤波器上的设计 DC电源模块在电容滤波器上的设计是电源管理系统中非常重要的一部分&#xff0c;其目的是为了确保电源输出电压的稳定性和纹波尽可能小。在设计中&#xff0c;需要考虑到电源负载的变化和变压器等电源配件的电磁干扰等因素。下面我们详细…

基于Java的民宿管理系统设计与实现(源码+lw+部署文档+讲解等)(民宿预约、民宿预订、民宿管理、酒店预约通用)

文章目录 前言具体实现截图论文参考详细视频演示代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技…