Java学习苦旅(二十五)——哈希表

本篇博客将详细讲解哈希表。

文章目录

  • 哈希表
    • 概念
    • 冲突
      • 概念
      • 避免冲突
      • 哈希函数设计
        • 常见哈希函数
      • 负载因子调节
      • 解决冲突
        • 闭散列
        • 开散列(哈希桶)
    • 和java类集的关系
  • 结尾

哈希表

概念

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

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

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

数据集合{1,7,6,4,5,9};

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

image-20220317093656489

冲突

概念

对于两个数据元素的关键字ki和kj(i != j),有ki != kj,但有:Hash(ki) == Hash(kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

避免冲突

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

哈希函数设计

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

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间。

  • 哈希函数计算出来的地址能均匀分布在整个空间中。

  • 哈希函数应该比较简单

常见哈希函数
  1. 直接定制法

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

  1. 除留余数法

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

  1. 平方取中法

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。

平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。

  1. 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

  1. 随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。

通常应用于关键字长度不等时采用此法。

  1. 数学分析法

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况。

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

负载因子调节

image-20220317095320046

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

image-20220317095418472

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

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

解决冲突

解决哈希冲突两种常见的方法是:闭散列开散列

闭散列

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

寻找下一个位置的方法:

  1. 线性探测

比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

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

插入

  • 通过哈希函数获取待插入元素在哈希表中的位置。

  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

  • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

image-20220317095827528

  1. 二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi= (H0+i2)% m,或者:Hi= (H0-i2)% m。其中:i = 1,2,3…,H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。 对于如果要插入44,产生冲突,使用解决后的情况为:

image-20220317114304722

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

因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

此外,从JDK1.8开始,当链表长度超过8,数组长度超过64,这个链表就会变成红黑树。

开散列(哈希桶)

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

image-20220317114513528

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了

示例代码

public class HashBuck {
    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;

    public static final double DEFAULT_LOAD_FACTOR = 0.75;

    public HashBuck() {
        this.array = new Node[10];
    }

    public void put(int key, int val) {
        //找到key所在位置
        int index = key % this.array.length;
        //遍历这个下标的链表,看是否有相同的key,如果有,则需更新value
        Node cur = array[index];
        while (cur != null) {
            if (cur.key == key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //没有这个key这个元素,头插法
        Node node = new Node(key,val);
        node.next = array[index];
        array[index] = node;
        this.usedSize++;
        //插入元素之后,检查当前列表的负载因子
        if (loadFactor() >= DEFAULT_LOAD_FACTOR) {
            resize();
        }
    }

    private void resize() {
        Node[] newArray = new Node[2*array.length];
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while (cur != null) {
                int index = cur.key % newArray.length;//获取新的下标
                //将cur节点以头插或者尾插插入新的数组对应下标的链表中
                Node curNext = cur.next;
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        array = newArray;
    }


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

    /**
     * 根据key得到value
     * @param key
     * @return
     */
    public int get(int key) {
        int index = key % this.array.length;
        Node cur = array[index];
        while (cur != null) {
            if (cur.key == key) {
                return cur.val;
            }
        }
        return -1;
    }
}

虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1) 。

和java类集的关系

  1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set。

  2. java 中使用的是哈希桶方式解决冲突的。

  3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)。

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

结尾

本篇博客到此结束。
上一篇博客:Java学习苦旅(二十四)——Java中的内部类
下一篇博客:Java学习苦旅(二十六)——反射,枚举和lamda表达式

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Leetcod面试经典150题刷题记录 —— 链表篇

Leetcod面试经典150题刷题记录-系列Leetcod面试经典150题刷题记录——数组 / 字符串篇Leetcod面试经典150题刷题记录 —— 双指针篇Leetcod面试经典150题刷题记录 —— 矩阵篇Leetcod面试经典150题刷题记录 —— 滑动窗口篇Leetcod面试经典150题刷题记录 —— 哈希表篇Leetcod面…

【设计模式-01】Singleton单利模式

一、方式1(最常用&#xff0c;推荐使用) 单例实现方式一: 饿汉式 类加载到内存后&#xff0c;就实例化一个单例&#xff0c;JVM保证线程安全 简单实用&#xff0c;推荐使用。 唯一缺点: 不管用到与否&#xff0c;类装载时就完成加载。 /*** description: 单例实现方式一: 饿汉…

Python之Selenium自动化浏览器测试详解

Python之Selenium(自动化浏览器测试) 1.安装selenium 1 pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple 2.下载对应版本的浏览器驱动 CNPM Binaries Mirror 这是我的。 把解压后的驱动放在自己的python.exe 目录下。 3.测试code&#xff0c;打开一个网页…

大甩卖——代码全家桶!!!

Python-凯斯西储大学&#xff08;CWRU&#xff09;轴承数据解读与分类处理 Python轴承故障诊断 (一)短时傅里叶变换STFT Python轴承故障诊断 (二)连续小波变换CWT_pyts 小波变换 故障-CSDN博客 Python轴承故障诊断 (三)经验模态分解EMD_轴承诊断 pytorch-CSDN博客 Pytorch-…

四 视图

1、实验目的 理解SQL成熟设计基本规范&#xff0c;能够熟练使用SQL语句来创建需要的视图&#xff0c;定义数据库外模式&#xff0c;并能使用所创建的视图实现数据管理。 2、实验内容及要求 使用SQL对数据库进行各类查询数据操纵操作&#xff0c;掌握单行数据插入、多行数据插…

开发小技巧 - 合理使用Visual Studio 2022内置任务列表(TODO)

前言 在开发编码过程中经常会因为各种问题而打断自己的思绪和开发计划&#xff0c;可能会导致本来准备开发或者需要测试的功能到要上线的时候才想起来没有做完。这种情况相信很多同学都遇到过&#xff0c;咱们强大的Visual Studio内置了一个任务列表&#xff08;TODO&#xff…

NVIDIA深入理解之pynvml库

一、前言 写在前面 该文章是对我之前文章《Fedora上安装NVIDIA闭源显卡驱动》的一个拓展&#xff0c;正好寒假闲的没事干不如加深一下对NVIDIA的了解。Python是当前非常流行的一门编程语言&#xff0c;它以kiss为设计思想&#xff0c;能封装就能封装&#xff0c;给用户提供比…

Tmux 使用小记

本文参考自 阮一峰老师Tmux 使用教程[1] Tmux,不仅仅是分屏那么简单。。。 与tmux类似的工具是screen 会话管理 将窗口与会话"解绑" 对于没有图形界面只有shell的场景(如服务器)&#xff0c;尤其有用..这是其最核心解决的问题(窗口管理啥的只能算锦上添花的辅助功能)…

想要成为机器学习领域的高手吗?这里有五本必读免费书,订阅周报发链接 (下)

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

腾讯云com域名注册1元一年,非常可以!

腾讯云com域名注册优惠价格1元首年&#xff0c;条件是企业新用户&#xff0c;个人新用户注册com域名是33元首年&#xff0c;第二年续费价格85元一年。活动 txybk.com/go/domain-sales 活动打开如下图&#xff1a; 腾讯云com域名注册优惠价格 腾讯云com域名注册原价是85元一年&a…

2024年数学建模美赛能用chatGPT之类的AI吗?官方给了明确规定!

这两年chatGPT等大语言模型火了&#xff0c;能对话&#xff0c;自然也能回答数学建模方面的问题。 那美赛能不能用这些AI呢&#xff1f;2024年美赛官方对chatGPT等的使用做出了明确的规定&#xff08;其中的VI. Contest Instructions部分&#xff09;&#xff1a; https://ww…

uniapp微信小程序投票系统实战 (SpringBoot2+vue3.2+element plus ) -投票创建页面实现

锋哥原创的uniapp微信小程序投票系统实战&#xff1a; uniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )_哔哩哔哩_bilibiliuniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )共计21条视频…

程序猿的时间管理和生产力

文章目录 为什么时间管理很重要&#xff1f;如何管理时间&#xff1f;心理维度生理维度技术尺寸 时间管理技巧每周计划基于目标的规划番茄钟为什么是25分钟&#xff1f;番茄钟为什么有效&#xff1f;艾森豪威尔矩阵这一切都是从开发者的角度来看的 也许我从开始学习或从事软件开…

企鹅目标检测数据集VOC格式400张

企鹅&#xff0c;一种可爱而独特的鸟类&#xff0c;以其圆滚滚的身体、黑白相间的羽毛和独特的行走方式而备受人们喜爱。 企鹅是鸟纲、企鹅科的动物&#xff0c;它们生活在南半球&#xff0c;特别是南极地区。企鹅的体型短而肥胖&#xff0c;有着流线型的身体和黑白相间的羽毛…

支持API文档生成,API管理工具:Apipost

随着数字化转型的加速&#xff0c;API&#xff08;应用程序接口&#xff09;已经成为企业间沟通和数据交换的关键。而在API开发和管理过程中&#xff0c;API文档、调试、Mock和测试的协作显得尤为重要。Apipost正是这样一款一体化协作平台&#xff0c;旨在解决这些问题&#xf…

条款19:设计class犹如设计type

设计class时&#xff0c;都要面对如下问题&#xff0c;答案通常会导致你的设计规范&#xff1a; 如何创建和销毁新类型的对象&#xff1f;这将影响 类的构造函数和析构函数的设计 内存分配和释放函数的设计 (operator new, operator new[], operator delete, operator delete[…

CSS基础选择器

1.CSS选择器&#xff08;重点&#xff09; 理解 能说出选择器的作用 id选择器和类选择器的区别 应用 能够使用基础选择器给页面元素添加样式 如图所以&#xff0c;要把里面的小黄人分为2组&#xff0c;最快的方法怎办&#xff1f; 1.1 选择器的作用 找到特定的HTML页面元…

金蝶EAS pdfviewlocal.jsp接口存在任意文件读取漏洞 附POC软件

免责声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者无关。该文章仅供学习用途使用。 1. 金蝶EAS简介 微信公众号搜索:南风漏洞复现文库 该…

分布式「走进分布式一致性协议」从2PC、3PC、Paxos 到 ZAB

设计一个分布式系统必定会遇到一个问题—— 因为分区容忍性&#xff08;partition tolerance&#xff09;的存在&#xff0c;就必定要求我们需要在系统可用性&#xff08;availability&#xff09;和数据一致性&#xff08;consistency&#xff09;中做出权衡 。这就是著名的 C…