哈希表查找

本文详细介绍了哈希表的工作原理,包括哈希函数的构建(直接地址法和除留余数法)、冲突解决策略(开放地址法、链地址法和再哈希法),并重点讨论了HashMap的插入方式及其与Hashtable的区别。讲解了哈希表查找性能分析和典型例题,以及HashMap与线程安全的Hashtable在效率和设计上的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【参考】
【1】数据结构(Java版)吕云翔,郭颖美
【2】数据结构 杨剑
【3】数据结构教程 王少波,张志

哈希表

1.哈希(hash)表的概念

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

1.1 哈希冲突

如果两个hash值相同,称为hash冲突。如下: H ( k 1 ) = H ( k 2 ) H (k1) = H(k2) H(k1)=Hk2

2.哈希函数的构建

下面介绍两种构造hash函数的常用方法。

2.1.直接地址法

直接定址法即:

H ( k e y ) = a ∗ k e y + b ( a 、 b 为 常 数 ) H (key) = a*key + b (a、b为常数) H(key)=akey+b(ab)

它是取关键字的某个线性函数值为hash地址。

直接地址法简单,不会产生冲突,但是关键字往往是离散的,且关键字集合比hash地址大,会造成存储空间的浪费。

2.2.除留余数法

除留余数法即:

H e y ( k e y ) = k e y    m o d    p ( p < = m ) Hey(key)=key ~~ mod~~ p (p <= m) Hey(key)=key  mod  p(p<=m)

它是以关键字除 p p p的余数作为hash地址,其中 m m m为hash表的长度。

使用除留余数法 p p p的选择很重要,否则会造成严重的冲突。例如,若取 p = 2 k p = 2^k p=2k,则 H ( k e y ) = k e y    m o d    p H(key)=key ~~mod~~ p H(key)=key  mod  p的值仅仅是用二进制表示的 k e y key key右边的 k k k个位,造成了关键字的映射并不均匀,易造成冲突。通常,为了获取比较均匀的地址分布,一般令 p p p位小于等于 m m m的某个最大元素。

3.解决冲突的方法

无论如何构造哈希函数,冲突都是不可避免的,因此如何处理冲突是建hash表的一个重要方面。

3.1.开放地址法

开放地址法的基本思想是:在发生冲突时,按照某种方法继续探测基本表中的其他存储单元,直到找到空位置为止。可用下式描述:

H i ( k e y ) = ( H ( k e y ) + d i )    m o d    m ( i = 1 , 2 , . . . , k ( k < = m − 1 ) ) H_{i}(key) = (H (key) + d_{i})~~ mod~~ m (i = 1, 2, ..., k (k <= m - 1)) Hi(key)=(H(key)+di)  mod  m(i=1,2,...,k(k<=m1))

其中, H ( k e y ) H(key) H(key)为关键字 k e y key key的直接hash地址, m m m为hash表长, d i d_{i} di为每次再探测地址增量。

用这种方法时,首先计算出它的直接hash地址 H ( k e y ) H(key) H(key),若该单元已被其他记录占用,继续查找地址为 H ( k e y ) + d 1 H(key)+d_{1} Hkey)+d1的单元,若也被占用,再继续查看地址为 H ( k e y ) + d 2 H(key)+d_{2} H(key)+d2的单元,如此下去,当发现某个单元为空时,将关键字为key的记录存放到该单元中。
增量 d i d_{i} di可以有不同的取法:
(a) d i = 1 , 2 , . . . , m − 1 d_{i}=1,2,...,m-1 di=1,2,...,m1
(b) d i = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , k 2 , − k 2 ( k < = m / 2 ) d_{i}=1^2,-1^2,2^2,-2^2,...,k^2,-k^2(k<=m/2) di=12,12,22,22,...,k2,k2(k<=m/2)
(c) d i = 伪 随 机 序 列 d_{i}=伪随机序列 di=

d i d_{i} di采用上述3种不同的取值方法时,分别称为线性探测再散列、二次探测再散列、和伪随机再散列。

3.2.链地址法

链地址法是把具有相同哈希地址的关键字放在同一个链表中。若选定的哈希表长度为 m m m,则可以将哈希表定义为一个由 m m m个头指针组成的指针数组T,凡是哈希地址为 i i i的节点,均插入到 T [ i ] T[i] T[i]为头节点的单链表中。 T T T中各分量的初值均应该为空。

例如,给定关键字集合{ 19,01,23,14,55,68,11,82,36 },取哈希表长为 m = 7 m=7 m=7,散列函数为 H ( k e y ) = k e y    m o d    7 H(key)=key~~mod~~7 H(key)=key  mod  7,用链地址法解决冲突所构造出来的哈希表,如下图
在这里插入图片描述

3.3.公共溢出区

公共溢出区域法是另建一个溢出表,当不发生冲突时,数据元素存入基本表,当发生冲突时数据元素存入溢出表。

3.4.再哈希法

再哈希法是当发生冲突时,再使用另一个哈希函数得到一个新的哈希地址,若再发生冲突,则再使用另一个函数,直到不发生冲突为止。这种方法需要预先设计一个哈希函数序列:
H i = R H i ( k e y )    ( i = 1 , 2 , 3 , . . . , k ) H_{i}=RH_{i}(key)~~(i=1,2,3,...,k) Hi=RHi(key)  (i=1,2,3,...,k)
这种方法不易产生“聚集”现象,但会增加计算的时间。

4.哈希表查找性能分析

使用平均查找长度来衡量哈希表的查找效率,在查找过程中与关键字比较的比较次数取决于hash函数的选取和处理冲突的方法。
假设hash函数是均匀的,即对同样的一组随机关键字出现冲突的可能是相同的。因此,哈希表的查找效率主要取决于处理冲突的方法。发生冲突的次数和hash表的装填因子有关,哈希表的装填因子如下:

a = 哈 希 表 中 数 据 元 素 个 数 / 哈 希 表 的 长 度 a=哈希表中数据元素个数/哈希表的长度 a=/

填入表中的数据元素越多, a a a越大,产生冲突的可能性越大;填入表中的数据元素越少, a a a越小,产生冲突的可能性越小, a a a通常取1和0.5之间的较小数(一般取0.75左右)。

5.习题

例题:已知关键码序列为(Jan, Feb, Mar, Apr, May, Jun, Jul ,Aug, Sep, Oct, Nov, Dec),散列表的地址空间为 0 − 16 0-16 016,设散列函数为 H ( x ) = i / 2 H(x)=i/2 H(x)=i/2(向下取整数),其中 i i i为关键码中第一个字母在字母表中的序号,采用线性探测法和链地址法处理冲突,试分别构造散列表并等概率情况下查找成功的平均查找长度。
在这里插入图片描述
在这里插入图片描述

HashMap

HashMap是一个存储键值对的集合,也就是我们常说的key,value,其中每一个存储位置都是一个Entry对象,Entry对象里有key,value,key的hash值,下一个对象的地址next,初始化的时候,每一个数组元素都为空。
hashMap的散列函数是 index = HashCode(key)& (length-1)(length必须是2的幂次,这样散列表比较均匀);

元素插入方式

在JDK1.8中采用的是尾插法。

在JDK1.6,JDK1.7中采用的是头插法。

1.put()方法:先对要插入的元素进行hash,找到数组当中对应的索引。
插入方式有四种情况(JDK1.8):

  • slot为空,直接插入

  • slot不为空

    • 如果key相同,进行替换
    • 如果key不同,采用尾插法
  • 有红黑树的插入。

数据结构

   Entry(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

  1. get() 方法:可以帮助我们通过key来找到value.首先会把key做一次hash得到对应的数组位置。
  2. Hash算法:hashMap的散列函数是 index = HashCode(key)& (length-1)(length必须是2的幂次,这样散列表比较均匀);
  3. hashmap扩容:由于HashMap的容量非常有限,当多次元素插入的时候,使得HashMap达到一定的饱和程度以后,key的映射发生冲突的概率比较高,这个时候HashMap需要拓展他的数组长度,也就是HashMap扩容,影响扩容的因素有两个,第一个Capacity(初始值16),这个是HashMap当前的最大数组容量。第二个LoadFactor,这个是HashMap的负载因子(默认值为0.75)。判断HashMap是否扩容条件(HashMap.size>=Capacity*LoadFactor),满足这个条件以后,那么会创建一个新的Entry数组,长度为原来的两倍,之后会遍历原来的Entry数组,把之前的Entry数组重新Hash到新的数组中。
  4. 红黑树:JDK1.8之前:数组+链表,JDK1.8及以后采用的是数组+链表+红黑树。当链表长度小于8的时候依然采用的是数组加链表的实现方式,但是当节点数大于8的时候(数组长度>=64),采用的是红黑树来实现,目的是提高查找效率。

Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表,为啥呢?

根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

HashMap和Hashtable的区别

  1. 线程安全: HashMap 是线程不安全的,HashTable 是线程安全的;HashTable 内部的方法基本都用 synchronized 修饰。
  2. 效率:因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰。
  3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键 所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
  4. 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来 的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充 为2的幂次方大小(HashMap 中的 tableSizeFor() 方法保证,下面给出了源代码)。也就是说 HashMap 总 是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

Hashtable与ConcurrentHashMap

1.对比HashMap,Hashtable本身是一个线程安全的集合类,在修改数据的时候会锁住整个哈希表,执行效率比较低,在多线程进行操作的时候,基本上就等同于串行。

ConcurrentHashMap他通过把整个Map划分为16Segment,在对一Segment进行写操作的时候,会把这个Segment加锁,其他线程就无法进入这个Segment进行操作,但是其他线程的读写操作并不受影响,相比Hashtable,他的性能总体上是提升了16倍。但是读操作并不会加锁,而是采用volatile这个关键字修饰,保证可读操作的一个可见性,可以保证我们的线程不会读到脏数据,总结起来,HashMap是线程不安全的,但是他的效率比较高。 Hashtable与ConcurrentHashMap,其中Hashtable采用的是锁住这个对象效率比较低,虽然线程安全但是效率极低,ConcurrentHashMap采用了分段锁,效率高,同时也可以保证线程的安全。并发量情况较大的时候,建议采用ConcurrentHashMap。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值