哈希表--想彻底了解哈希表,看这一篇文章就可以了

news2024/12/28 18:25:30

为了一次存储便能得到所查记录,在记录的存储位置和它的关键字之间建立一个确定的对应关系H,已H(key)作为关键字为key的记录在表中的位置,这个对应关系H为哈希(Hash)函数, 按这个思路建立的表为哈希表。

哈希表也叫散列表。从根本上来说,一个哈希表包含一个数组,通过特殊的关键码(也就是key)来访问数组中的元素。

哈希表的主要思想:
(1)存放Value的时候,通过一个哈希函数,通过关键码(key)进行哈希运算得到哈希值,然后得到映射的位置, 去寻找存放值的地方 ,

(2)读取Value的时候,也是通过同一个哈希函数,通过关键码(key)进行哈希运算得到哈希值,然后得到 映射的位置,从那个位置去读取。

哈希函数

哈希表的组成取决于哈希算法,也就是哈希函数的构成。哈希函数计算过程会将键转化为数组的索引。

一个好的哈希函数至少具有两个特征:
(1)计算要足够快;
(2)最小化碰撞,即输出的哈希值尽可能不会重复。

那接下来我们就来看下几个常见的哈希函数:

直接定址法

  • 取关键字或关键字的某个线性函数值为散列地址。
  • 即 f(key) = key 或 f(key) = a*key + b,其中a和b为常数。

除留余数法

将整数散列最常用方法是除留余数法。除留余数法的算法实用得最多。

我们选择大小为m的数组,对于任意正整数k,计算k除以m的余数,即f(key)=k%m,f(key)<m。这个函数的计算非常容易(在Java中为k% M)并能够有效地将键散布在0到M-1的范围内。

数字分析法

  • 当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
  • 仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。

平方取中法

  • 先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
  • 随机分布的关键字,得到的散列地址也是随机分布的。

随机数法

  • 选择一个随机函数,把关键字的随机函数值作为它的哈希值。
  • 通常当关键字的长度不等时用这种方法。

每种数据类型都需要相应的散列函数.

例如,Interge的哈希函数就是直接获取它的值:

public static int hashCode(int value) {
    return value;
}

对于字符串类型则是使用了s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]的算法:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        hash = h = isLatin1() ? StringLatin1.hashCode(value)
                              : StringUTF16.hashCode(value);
    }
    return h;
}

public static int hashCode(byte[] value) {
    int h = 0;
    for (byte v : value) {
        h = 31 * h + (v & 0xff);
    }
    return h;
}
public static int hashCode(byte[] value) {
    int h = 0;
    int length = value.length >> 1;
    for (int i = 0; i < length; i++) {
        h = 31 * h + getChar(value, i);
    }
    return h;
}

double类型则是使用位运算的方式进行哈希计算:

public int hashCode() {
    long bits = doubleToLongBits(value);
    return (int)(bits ^ (bits >>> 32));
}
public static long doubleToLongBits(double value) {
    long result = doubleToRawLongBits(value);
    if ( ((result & DoubleConsts.EXP_BIT_MASK) == DoubleConsts.EXP_BIT_MASK)
    &&
   (result & DoubleConsts.SIGNIF_BIT_MASK) != 0L)
        result = 0x7ff8000000000000L;
    return result;
}

于是Java让所有数据类型都继承了超类Object类,并实现hashCode()方法。接下来我们看下Object.hashcode方法。Object类中的hashcode方法是一个native方法。

 public native int hashCode();

hashCode 方法的实现依赖于jvm,不同的jvm有不同的实现,我们看下主流的hotspot虚拟机的实现。hotspot 定hashCode方法在src/share/vm/prims/jvm.cpp中,源码如下:

JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
  JVMWrapper("JVM_IHashCode");
  return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
JVM_END

接下来我们看下ObjectSynchronizer::FastHashCode 方法是如何返回hashcode的,ObjectSynchronizer::FastHashCode 在synchronized.hpp文件中,

intptr_t ObjectSynchronizer::identity_hash_value_for(Handle obj) {
  return FastHashCode (Thread::current(), obj()) ;
}

intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
  if (UseBiasedLocking) {
   
    if (obj->mark()->has_bias_pattern()) {
      // Box and unbox the raw reference just in case we cause a STW safepoint.
      Handle hobj (Self, obj) ;
      // Relaxing assertion for bug 6320749.
      assert (Universe::verify_in_progress() ||
              !SafepointSynchronize::is_at_safepoint(),
             "biases should not be seen by VM thread here");
      BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());
      obj = hobj() ;
      assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
    }
  }


  ObjectMonitor* monitor = NULL;
  markOop temp, test;
  intptr_t hash;
  // 获取调用hashCode() 方法的对象的对象头中的mark word 
  markOop mark = ReadStableMark (obj);

  // object should remain ineligible for biased locking
  assert (!mark->has_bias_pattern(), "invariant") ;

  if (mark->is_neutral()) {  //普通对象
    hash = mark->hash();              // this is a normal header
    //如果mark word 中已经保存哈希值,那么就直接返回该哈希值
    if (hash) {                       // if it has hash, just return it
      return hash;
    }
    // 如果mark word 中还不存在哈希值,那就调用get_next_hash(Self, obj)方法计算该对象的哈希值
    hash = get_next_hash(Self, obj);  // allocate a new hash code
   // 将计算的哈希值CAS保存到对象头的mark word中对应的bit位,成功则返回,失败的话可能有几下几种情形:
   //(1)、其他线程也在install the hash并且先于当前线程成功,进入下一轮while获取哈希即可
   //(2)、有可能当前对象作为监视器升级成了轻量级锁或重量级锁,进入下一轮while走其他case;
    temp = mark->copy_set_hash(hash); // merge the hash code into header
    // use (machine word version) atomic operation to install the hash
    test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
    if (test == mark) {
      return hash;
    }
    // If atomic operation failed, we must inflate the header
    // into heavy weight monitor. We could add more code here
    // for fast path, but it does not worth the complexity.
  } else if (mark->has_monitor()) { //重量级锁
  // 果对象是一个重量级锁monitor,那对象头中的mark word保存的是指向ObjectMonitor的指针,
  //此时对象非加锁状态下的mark word保存在ObjectMonitor中,到ObjectMonitor中去拿对象的默认哈希值:
    monitor = mark->monitor();
    temp = monitor->header();
    assert (temp->is_neutral(), "invariant") ;
    hash = temp->hash();
  //(1)如果已经有默认哈希值,则直接返回;
    if (hash) {
      return hash;
    }
    // Skip to the following code to reduce code size
  } else if (Self->is_lock_owned((address)mark->locker())) {  //轻量级锁锁
  //如果对象是轻量级锁状态并且当前线程持有锁,那就从当前线程栈中取出mark word:
 
    temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned
    assert (temp->is_neutral(), "invariant") ;
    hash = temp->hash();              // by current thread, check if the displaced
     //(1)如果已经有默认哈希值,则直接返回;
    if (hash) {                       // header contains hash code
      return hash;
    }
   
  }

  // Inflate the monitor to set hash code
  monitor = ObjectSynchronizer::inflate(Self, obj);
  // Load displaced header and check it has hash code
  mark = monitor->header();
  assert (mark->is_neutral(), "invariant") ;
  hash = mark->hash();
  //计算默认哈希值并保存到mark word中后再返回
  if (hash == 0) {
    hash = get_next_hash(Self, obj);
    temp = mark->copy_set_hash(hash); // merge hash code into header
    assert (temp->is_neutral(), "invariant") ;
    test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
    if (test != mark) {
     
      hash = test->hash();
      assert (test->is_neutral(), "invariant") ;
      assert (hash != 0, "Trivial unexpected object/monitor header usage.");
    }
  }
  // We finally get the hash
  return hash;
}

关于对象头、java内置锁的内容请阅读《高并发核心编程:卷2》。

ObjectSynchronizer :: FastHashCode()也是通过调用identity_hash_value_for方法返回值的. 调用了get_next_hash()方法生成hash值,源码如下:

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) { //随机数 openjdk6、openjdk7 采用的是这种方式
     // This form uses an unguarded global Park-Miller RNG,
     // so it's possible for two threads to race and generate the same RNG.
     // On MP system we'll have lots of RW access to a global, so the
     // mechanism induces lots of coherency traffic.
     value = os::random() ;
  } else
  if (hashCode == 1) { //基于对象内存地址的函数
     // This variation has the property of being stable (idempotent)
     // between STW operations.  This can be useful in some of the 1-0
     // synchronization schemes.
     intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
  } else
  if (hashCode == 2) { //恒等于1(用于敏感性测试)
     value = 1 ;            // for sensitivity testing
  } else
  if (hashCode == 3) { //自增序列
     value = ++GVars.hcSequence ;
  } else
  if (hashCode == 4) {  //将对象的内存地址强转为int
     value = cast_from_oop<intptr_t>(obj) ;
  } else { 
    //生成hash值的方式六: Marsaglia's xor-shift scheme with thread-specific state
  //(基于线程具体状态的Marsaglias的异或移位方案) openjdk8之后采用的就是这种方式
     // Marsaglia's xor-shift scheme with thread-specific state
     // This is probably the best overall implementation -- we'll
     // likely make this the default in future releases.
     unsigned t = Self->_hashStateX ;
     t ^= (t << 11) ;
     Self->_hashStateX = Self->_hashStateY ;
     Self->_hashStateY = Self->_hashStateZ ;
     Self->_hashStateZ = Self->_hashStateW ;
     unsigned v = Self->_hashStateW ;
     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
     Self->_hashStateW = v ;
     value = v ;
  }

  value &= markOopDesc::hash_mask;
  if (value == 0) value = 0xBAD ;
  assert (value != markOopDesc::no_hash, "invariant") ;
  TEVENT (hashCode: GENERATE) ;
  return value;
}

到底用的哪一种计算方式,和参数hashCode有关系,在src/share/vm/runtime/globals.hpp中配置了默认:
openjdk6:

  product(intx, hashCode, 0,                                       \
  "(Unstable) select hashCode generation algorithm")                \                                                
        

openkjdk8:

  product(intx, hashCode, 5,                                       \
  "(Unstable) select hashCode generation algorithm")                \

也可以通过虚拟机启动参数-XX:hashCode=n来做修改。

到这里你知道hash值是如何生成的了吧。

哈希表因为其本身结构使得查找对应的值变得方便快捷,但是也带来了一些问题,问题就是无论使用哪种方式生成hash值,总有产生相同值的时候。接下来我们就来看下如何解决hash值相同的问题。

hash 碰撞(哈希冲突)

对于两个不同的数据元素通过相同哈希函数计算出来相同的哈希地址(即两不同元素通过哈希函数取模得到了同样的模值),这种现象称为哈希冲突或哈希碰撞。

一般来说,哈希冲突是无法避免的。如果要完全避免的话,那么就只能一个字典对应一个值的地址,这样一来, 空间就会增大,甚至内存溢出。减少哈希冲突的原因是Hash碰撞的概率就越小,map的存取效率就会越高。 常见的哈希冲突的解决方法有开放地址法和链地址法:

开放地址法

开放地址法又叫开放寻址法、开放定址法,当冲突发生时,使用某种探测算法在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。开放地址法需要的表长度要大于等于所需要存放的元素。

按照探测序列的方法,可以细分为线性探查法、平法探查法、双哈希函数探查法等。

这里为了更好的展示三种方法的效果,我们用例子来看看:设关键词序列为{47,7,29,11,9,84,54,20,30},哈希表长度为13,装载因子=9/13=0.69,哈希函数为f(key)=key%p=key%11

关键词(key)4772911984542030
散列地址k(key)3770971098

(1)线性探测法

当我们的所需要存放值的位置被占了,我们就往后面一直加1并对m取模直到存在一个空余的地址供我们存放值,取模是为了保证找到的位置在0~m-1的有效空间之中。

公式:fi=(f(key)+i) % m ,0 ≤ i ≤ m-1i会逐渐递增加1)

具体做法: 探查时从地址d开始,首先探查T[d],然后依次探查T[d+1]…直到T[m-1],然后又循环到T[0]、T[1],…直到探查到有空余的地址或者直到T[d-1]为止。

用线性探测法处理冲突得到的哈希表如下

缺点:需要不断处理冲突,无论是存入还是査找效率都会大大降低。

(2)平方探查法

当我们的所需要存放值的位置被占了,会前后寻找而不是单独方向的寻找。

公式:fi=(f(key)+di) % m,0 ≤ i ≤ m-1

具体操作:探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+di],di 为增量序列12、-12,22、-22, ……,q2、-q2 且q≤1/2 (m-1) ,直到探查到 有空余地址或者到 T[d-1]为止。

用平方探查法处理冲突得到的哈希表如下
在这里插入图片描述

(3)双哈希函数探查法
公式:fi=(f(key)+i*g(key)) % m (i=1,2,……,m-1)
其中f(key) 和g(key) 是两个不同的哈希函数,m为哈希表的长度。

具体步骤:
双哈希函数探测法,先用第一个函数f(key)对关键码计算哈希地址,一旦产生地址冲突,再用第二个函数 g(key)确定移动的步长因子,最后通过步长因子序列由探测函数寻找空的哈希地址。

开发地址法,通过持续的探测,最终找到空的位置。为了解决这个问题,引入了链地址法。

链地址法

在哈希表每一个单元中设置链表,某个数据项对的关键字还是像通常一样映射到哈希表的单元中,而数据项本身插入到单元的链表中.

链地址法简单理解如下:
在这里插入图片描述

来一个相同的数据,就将它插入到单元对应的链表中,在来一个相同的,继续给链表中插入。

链地址法解决哈希冲突的例子如下:
(1)采用除留余数法构造哈希函数,而 冲突解决的方法为 链地址法。
(2)具体的关键字列表为(19,14,23,01,68,20,84,27,55,11,10,79),则哈希函数为f(key)=key MOD 13。则采用除留余数法和链地址法后得到的预想结果应该为:
在这里插入图片描述

哈希造表完成后,进行查找时,首先是根据哈希函数找到关键字的位置链,然后在该链中进行搜索,如果存在和关键字值相同的值,则查找成功,否则若到链表尾部仍未找到,则该关键字不存在。

哈希表作为一个非常常用的查找数据结构,它能够在O(1)的时间复杂度下进行数据查找,时间主要花在计算hash值上。在Java中,典型的Hash数据结构的类是HashMap。

然而也有一些极端的情况,最坏的就是hash值全都映射在同一个地址上,这样哈希表就会退化成链表,例如下面的图片:
在这里插入图片描述

当hash表变成图2的情况时,查找元素的时间复杂度会变为O(n),效率瞬间低下,

所以,设计一个好的哈希表尤其重要,如HashMap在jdk1.8后引入的红黑树结构就很好的解决了这种情况。

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

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

相关文章

【Leetcode】 1071. 字符串的最大公因子

For two strings s and t, we say “t divides s” if and only if s t ... t (i.e., t is concatenated with itself one or more times). Given two strings str1 and str2, return the largest string x such that x divides both str1 and str2. Example 1: Input: s…

开发改了接口,经常忘通知测试的解决方案!

目录 前言&#xff1a; Apifox解决方案 Apifox对此给出的解决方案是&#xff1a; 用Apifox怎么处理接口变更 接口代码实现逻辑修改 接口参数修改 前言&#xff1a; 在开发过程中&#xff0c;接口变动十分频繁&#xff0c;测试人员没有及时获得相关通知的情况也很普遍。这…

这一次AI应该是真正的已经到来

渐渐感觉这一次AI的变革能真正的突破迷雾&#xff0c;迎来真正的人工智能时代的到来。所以写篇博文学习一下。经过半年的发酵与发展&#xff0c;不得不说AI已经成为了不可逆转的趋势。之所以说这一次AI应该是真正的已经到来&#xff0c;是因为人工智能的发展其实已经经历了几十…

【Python】一文带你了解 正则表达式 + 简单操作

作者主页&#xff1a;爱笑的男孩。的博客_CSDN博客-深度学习,活动,python领域博主爱笑的男孩。擅长深度学习,活动,python,等方面的知识,爱笑的男孩。关注算法,python,计算机视觉,图像处理,深度学习,pytorch,神经网络,opencv领域.https://blog.csdn.net/Code_and516?typeblog个…

通过第三方软件修改 MacOS 的键盘映射

通过第三方软件修改 MacOS 的键盘映射 由于文本编辑时大量使用word level的左移、右移&#xff0c;其中&#xff1a; OSX的单词级左右移为option Left/Right Arrow&#xff0c;整行级左右移为command Left/Right Arrow 单词级移动与进行编辑常用的command不同键位&#xff0c…

Chat2DB:阿里巴巴开源的聊天数据管理工具--实践

Chat2DB&#xff1a;阿里巴巴开源的聊天数据管理工具–实践 简介 ​ Chat2DB 是一款有开源免费的多数据库客户端工具&#xff0c;支持windows、mac本地安装&#xff0c;也支持服务器端部署&#xff0c;web网页访问。和传统的数据库客户端软件Navicat、DBeaver 相比Chat2DB集成了…

Jetpack Compose ——Row

当我们构建界面时&#xff0c;经常需要在Compose中使用Row布局来水平排列多个组件。Row提供了一种方便的方式来管理和定位子组件&#xff0c;使它们按照我们期望的方式呈现。 在Compose中&#xff0c;Row可以接受多个子组件作为参数&#xff0c;并根据指定的布局规则进行排列。…

桥梁监测方案-智慧桥梁监测管理系统

桥梁作为现代交通基础设施的重要组成部分&#xff0c;承载着大量的车辆和行人交通。然而&#xff0c;随着时间的推移&#xff0c;桥梁结构可能会受到自然环境、交通负荷和物质疲劳等因素的影响&#xff0c;从而导致潜在的风险和结构损坏。桥梁监测能够通过实时监测桥梁的结构状…

git一定要学会,加油

gitgit文档http://file:///F:/%E8%B5%84%E6%96%99%E5%A4%8D%E4%B9%A0/Git%E4%BC%98%E7%A7%80%E5%BC%80%E6%BA%90%E4%B9%A6%E7%B1%8D/Git%E5%BC%80%E6%BA%90%E4%B9%A6%E7%B1%8D/Pro%20Git%E4%B8%AD%E6%96%87PDF%E7%89%88.pdf init 初始化仓库 这个命令在当前目录下初始化一个 G…

安装Django 3.2的过程记录~

首先看下Django 3.2目前更新到哪些版本&#xff0c;用下面这条命令&#xff1a; pip install Django100.0.5与3.2有关的版本号有&#xff1a; 3.2a1, 3.2b1, 3.2rc1, 3.2, 3.2.1, 3.2.2, 3.2.3, 3.2.4, 3.2.5, 3.2.6, 3.2.7, 3.2.8, 3.2.9, 3.2.10, 3.2.11, 3.2.12, 3.2.13, …

MAC系统使用

查看端口占用情况 //如查看8080端口的占用情况 sudo lsof -i tcp:9901//比如我们想要释放Java占用的9901端口 PID 是 12420 kill -9 12420

C语言:写一个函数,实现一个整型有序数组的二分查找

题目&#xff1a; 写一个函数&#xff0c;实现一个整型有序数组的二分查找&#xff0c;找出要找的数字在数组中对应的下标。 思路&#xff1a; 总体思路&#xff1a; &#xff08;一&#xff09;自定义函数部分&#xff1a; &#xff08;1&#xff09;. 参数&#xff1a; int…

融云《社交泛娱乐出海作战地图》,手把手教你拿下出海“最后一公里”难题

社交泛娱乐出海&#xff0c;已然是当下互联网出海的主力。关注【融云全球互联网通信云】了解更多 不少国内互联网巨头都将社交泛娱乐作为海外市场重点布局赛道。从较为传统的音视频、短视频社交出海&#xff0c;到逐渐直播出海、元宇宙社交出海&#xff0c;社交泛娱乐出海版图…

微信小程序实现tab及瀑布流页面

一、效果 二、代码 复制代码可以直接用 1、json代码 {"usingComponents": {},"navigationStyle": "custom" } 2、xml代码 <!--pages/find_module/findPage.wxml--> <view class"container_serach"><!-- 搜索模块 -…

Java EE 企业级应用开发教程题库(第二版)

前言 Java EE这是一门偏向于实践的课&#xff0c;奈何考试理论居多。一学期想搞懂三个框架&#xff0c;嘿嘿&#xff0c;难哦&#xff01;如果你是大一大二的同学&#xff0c;认认真真学习&#xff0c;真的有用。如果你是大三的同学&#xff0c;像就业并且走这个方向的同学&…

VMware Horizon 8 运维系列(一)windows系统重启被禁用的ipv6功能

前言 Horizon 8 在创建桌面池之前,需要对模板虚拟机进行配置,其中会用到windows优化工具,通过禁用一些不必要的服务或功能,让系统达到优化的效果。 最近客户提出一个需求:需要对虚拟机开启ipv6功能,并同时使用ipv6地址。通过查看模板系统,ipv6已经被优化工具禁用,所以需…

SSM整合快速入门案例(二)

文章目录 前言一、表现层与前端数据传输协议定义二、表现层与前端数据传输协议实现三、异常处理器四、项目异常处理五、前后台协议联调实现CRUD- - 查询- - 添加- - 修改- - 删除 总结 前言 为了巩固所学的知识&#xff0c;作者尝试着开始发布一些学习笔记类的博客&#xff0c…

Netty核心源码剖析(五)

1.Netty核心组件EventLoop源码剖析 1>.NioEventLoop继承图 说明: ①.ScheduledExecutorService接口表示是一个定时任务接口,EventLoop可以接受定时任务; ②.EventLoop接口:Netty 接口文档说明该接口作用:一旦Channel注册了,就处理该Channel对应的所有I/O操作; ③.Single…

学成在线----day7

1、 视频处理-技术方案 1. 作业分片方案 掌握了xxl-job的分片广播调度方式&#xff0c;下边思考如何分布式去执行学成在线平台中的视频处理任务。 任务添加成功后&#xff0c;对于要处理的任务会添加到待处理任务表中&#xff0c;现在启动多个执行器实例去查询这些待处理任务…

FPGA基础知识-层次建模的概念

学习目标&#xff1a; 提示&#xff1a;这里可以添加学习目标 理解数字电路设计中自底向上和自顶向下的设计方法&#xff1b; 解释verilog中模块和模块实例之间的区别&#xff1b; 学习从4中不同的抽象角度来描述同一个模块&#xff1b; 解释仿真中的各个组成部分&#xf…