散列数据结构以及在HashMap中的应用

1. 为什么需要散列表?

对于线性表和链表而言,访问表中的元素,时间复杂度均为O(n)。即便是通过树结构存储数据,时间复杂度也为O(logn)。那么有没有一种方式可以将这个时间复杂度降为O(1)呢?当然有,这就是接下来要介绍的 散列表散列表是普通数组概念的推广。由于对于普通数组只要知道其下标位置就可以使用O(1)的时间内访问任意元素,如果存储空间允许,我们可以提供一个足够大的数组,为每个可能的关键字保留一个位置,这个位置也被称之” “,从而可以充分的利用直接寻址的技术优势,其实就是典型的空间换时间。

2. 散列函数

既然 散列表是对关键字进行计算,从而确定该关键字对应的数据在存储中的位置,在下文中统一称之为 “槽”,那么又该通过什么方式进行计算呢?其实这个方式就是 散列函数。散列函数的设计对于散列表的性能将起到决定性的作用。因为如果散列函数设计不当导致多个关键字计算出的结果都是同一个位置,即存在大量的 散列冲突(也可以称为 散列碰撞)。现如今存在的散列函数算法非常多, 通常的散列算法都是将关键字转换为自然数,然后通过除法或是乘法进行散列。一些简单的散列算法,比如关键字是整数直接使用求余法;关键字是字符串的话,一种可行的算法是每个字符的ASCII码相加之后对表的长度进行取模。对于同一类型的关键字的散列算法是多种多样的,但无论如何应该 尽可能的避免散列冲突并且保证其散列的结果是均匀分布的。之所以要尽可能的保证散列结果是均匀分布其实也是为了尽可能的避免散列冲突。

3.散列冲突以及冲突解决

但是 无论散列算法设计的多么完美,散列冲突它都是一定存在的。因为对于散列表的大小而言它是固定的,一旦你初始化之后就不会改变。但是对于元素而言是可以无限制的添加的,换句话说就是 散列表中的”槽”位,对于关键字来说总归是不够的,所以就会出现多个关键字通过散列函数计算出的”槽”位是相同的。

当散列冲突出现的时候,主要通过 开放寻址法完全散列法分离链接法等其他算法解决冲突

1.开放寻址法

在开放寻址法中,散列表中的每个槽位最多只会存储一个元素。当出现散列冲突的时候,就会从该槽位出发选择一个方向(向前或是向后)开始探测,(每次探测的距离为1则称之为线性探查,距离为某个数字的平方则称之为平方探查)只要散列表足够大,总归是可以找到一个可以存储的槽位,但是如此花费的时间是相当多的。更糟糕的是,即使散列表相对较空这样占据的槽位一旦开始形成,当后面出现本应该放到该槽位的关键字由于已被占据,而不得不进行探测寻找可以存储的槽位,这种现象也被称之为 聚集。除此之外可以采用 双重散列法,使用一组散列函数,知道找到空闲的位置为止,一种比较流行的做法是使用两个相对独立的散列函数hash1(),hash2()。当发生碰撞时,通过步长i进行探测。

(hash1(key) + i * hash2(key)) % TABLE_SIZE

这种双散列如果hash2()设计的不好将会是灾难性的。一个好的hash2()表现好的特征是:1.不会产生0索引、2.可以探测整个散列表

2.分离链接法

在分离链接法中,散列表中出现冲突时,可以通过链表的方式将元素连接起来,在对元素进行访问时,若发现该槽位中是一个链表则对该链表进行遍历。此种分离方式并不只是仅限于链表,比如一颗树或是另一个散列表都是可以的。比如即将在下文中提到的HashMap就是使用链表+红黑树来实现的。

3.再散列

如果散列表很多槽位已经被占据,name操作的运行时间将开始消耗过长,且插入操作可能失败。此时一种解决方法是建立另外一个大约两倍大的表,扫描整个原始散列表计算每个元素的新的槽位并将其插入到新的表中,整个操作就被称为 再散列。其实本质上就是通过扩容减少冲突。

4.完全散列法

虽然全域散列和完全散列具有良好的理论性能,但实现起来不太方便,前提条件也多。在实际应用上,往往会更偏向其他方式解决冲突。

4.动态扩容

因为散列表在创建的时候其大小是固定的,而关键字是不断被添加到但列表中,所以随着关键字的不断添加,产生散列冲突的概率就会越来越大。因此为了避免哈希冲突就需要扩大散列表的容量。当已被占据的”槽”的个数和散列表的大小的比例达到一定的阈值时,就开始执行散列表的扩容,而这个阈值也被称之为 加载因子(或 扩容因子)。在扩容的时候,往往需要对原来的关键字重新进行散列,但是通过某些技巧其实是可以避免再散列的情况,比如HashMap的源码中在扩容的时候就没有进行再散列,这一部分在下文将详细讲解。

5.散列在HashMap的应用

**1、散列函数

**

1 public int hashCode() {
 2     int h = hash;
 3     if (h == 0 && value.length > 0) {
 4         char val[] = value;
 5         for (int i = 0; i < value.length; i++) {
 6             h = 31 * h + val[i];
 7         }
 8         hash = h;
 9     }
10     return h;
11 }

在这里为什么选择31作为乘数,为什么不是偶数或其他奇质数3,5,…,33,37,97…等其他数字? 原因如下:
1. 31 是一个奇质数,如果选择偶数会导致乘积运算时数据溢出,造成数据丢失;
2. 哈希碰撞:实验数据表明乘数为大于等于31的奇质数碰撞概率很小,基本稳定;
3.哈希分布:实验数据表明乘数为大于等于31的奇质数哈希分布相对来说较为均匀。
4.另外在二进制中,2的5次方是32,那么也就是 31 * i == (i << 5) -i。这主要是说乘积运算可以使用位移提升性能,同时JVM 也会位移操作的优化

2、扰动函数

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在这里HashMap并没有直接将key的散列值返回,而是进行了一次干扰计算

(h = key.hashCode()) ^ (h >>> 16)

把哈希值右移16位,也就是自己长度的一般,之后再与原哈希值进行异或运算。这样做的目的就是混合哈希值中的高位和低位增大随机性,使得哈希分布更加均匀,减少碰撞。

3、初始化容量

 1 static final int MAXIMUM_CAPACITY = 1 << 30;
 2
 3 static final int tableSizeFor(int cap) {
 4     int n = cap - 1;
 5     n |= n >>> 1;
 6     n |= n >>> 2;
 7     n |= n >>> 4;
 8     n |= n >>> 8;
 9     n |= n >>> 16;
10     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
11
12 }

在这里进行初始化容量的时候,会不断进行或运算将二进制数都填上1,目的就是去寻找2的次幂的最小值。如传入的cap值为9则返回距离9最小的2的次幂值即16。那在这里为什么需要寻找2的次幂的最小值呢?

4、插入、链表树化 、红黑树

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

散列数据结构以及在HashMap中的应用

通过源码分析,HashMap增加元素的过程如下:
1. 如果散列表不存在或是其长度为0则进行一次扩容操作
2. 通过key的哈希值对散列表的长度进行与计算获得槽位
2.1 若该槽位对应的元素为空
直接添加一个节点,添加节点后需要判断是否超过负载阈值,超过则进行扩容。
2.2 该槽位存在值
2.2.1 判断key是否与当前的key一致
一致时,修改该元素,然后返回旧值。
2.2.2 判断该槽位对应的元素是否为树节点,这个树其实是一颗红黑树为树节点时,则进入putTreeVal()方法,这个方法要做的事简单的说就是”根据哈希值遍历树的结构,是否可以找到该key,若是可以找到就返回该节点,若是找不到就会新增的一个节点,并且平衡该树,最终返回一个空值”。putTreeVal()方法在新增节点的是后续返回null最终需要判断是否超过负载阈值,超过则进行扩容;修改节点时返回该节点数据,则将该树节点对应的值修改为当前的value并直接返回。
2.2.3 说明这个槽位对应的元素是一个链表
为链表时,则先对链表进行遍历,是否可以找到该key,若可以找到则将该元素,则将该节点的值修改为value并退出;找不到该key时,说明这是一个新增元素,所以会在链表的尾部在添加一个节点。添加完节点后还需要判断该链表的长度是否超过了阈值(默认是8),超过阈值后并且表的大小还要超过64,则会将该链表进行转成二叉树,然后在转成红黑树,在转换成树的时候也会记录各节点的在链表中的位置;否则也只会对该散列表进行扩容。最终判断是否超过负载阈值,超过则进行扩容。

4、负载因子

 1 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 2
 3 public HashMap(int initialCapacity) {
 4     this(initialCapacity, DEFAULT_LOAD_FACTOR);
 5 }
 6
 7 final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
 8     int s = m.size();
 9     if (s > 0) {
10         if (table == null) { // pre-size
11             float ft = ((float)s / loadFactor) + 1.0F;
12             int t = ((ft < (float)MAXIMUM_CAPACITY) ?  (int)ft : MAXIMUM_CAPACITY);
13             if (t > threshold)
14                 threshold = tableSizeFor(t);
15         }
16         else if (s > threshold)
17             resize();
18         for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
19             K key = e.getKey();
20             V value = e.getValue();
21             putVal(hash(key), key, value, false, evict);
22         }
23     }
24 }

负载因子是关键字与散列表大小的比值,它决定了数据量达到多少之后进行扩容,默认的负载因子为0.75。如果希望以更多的空间换时间,尽量避免散列碰撞,则可以手动指定更小的负载因子。

5、扩容元素拆分

当数组长度不足时,或是当前关键字与散列表大小的比值超过了负载因子则进行散列表的扩容。在jdk1.7中,散列表扩容时,需要进行再散列的操作,重新计算各个key在新表中的槽位。而在jdk1.8中,扩容机制进行了优化,已经不需要进行再散列了,而是通过该key新的哈希值与原来的散列表进行与运算【 key.hash()&oldCap==0】,如果为0,则不需要修改槽位,否则将该槽位移动到原来的位置+oldCap的位置,即【j+oldCap】。当红黑树扩容后的节点数小于 UNTREEIFY_THRESHOLD(默认是6)即小于7个节点数时,红黑树则会进行链化,因为链表在转成红黑树的时候,是有记录各节点在链表中的位置的,所以红黑树在转成链表的时候会相对简单很多。

6、查找

HashMap查找元素的过程如下:
1. 通过散列函数计算并且扰动后的哈希值
2. 若散列表为空或其大小为0,则直接返回null;
3. 根据计算出的哈希值与散列表的大小-1做与运算获得槽位【在表中的下标索引】
4. 该槽位的元素是否为树节点
4.1 不为树节点,按链表形式进行遍历
4.2 为树节点,则按照红黑树形式进行遍历

10、删除

HashMap删除元素的过程如下:
1. 通过散列函数计算并且扰动后的哈希值
2. 若散列表为空或其大小为0,则直接返回null;
3. 根据计算出的哈希值与散列表的大小-1做与运算获得槽位【在表中的下标索引】
4. 该槽位的元素是否为树节点
4.1 不为树节点,按链表形式进行删除
4.2 为树节点,则按照红黑树形式进行删除,删除之后会进行红黑树的平衡

Original: https://www.cnblogs.com/zhenjungan/p/14942449.html
Author: zhenjungan
Title: 散列数据结构以及在HashMap中的应用

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/581882/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • SpringBoot + Vue + ElementUI 实现后台管理系统模板 — 后端篇(四): 整合阿里云 短信服务、整合 JWT 单点登录

    (1) 相关博文地址: SpringBoot + Vue + ElementUI 实现后台管理系统模板 — 前端篇(一):搭建基本环境:https://www.cnblogs.c…

    Linux 2023年6月11日
    095
  • 离职,问题就解决了吗?

    刚入职场的那几年,我经常对工作有各种抱怨。回想起来,有两次冲动的不必要的离职,或者说应该干得更久一点。一旦有了离职的想法,整个人每天都纠结是去还是留,这种纠结成倍的放大焦虑,让自己…

    Linux 2023年6月6日
    0100
  • docker部署安装Nginx

    docker部署安装Nginx 前言 Nginx是一个高性能的HTTP和反向代理web服务器,同事也提供了IMAP/POP3/SMTP服务。特点: 轻量级的Web服务器/反向代理服…

    Linux 2023年6月6日
    085
  • 【C++基础】数据类型

    C++规定在创建一个变量或者产量时,必须要指定相应的数据类型,否则无法给变量分配内存空间 数据类型的存在意义:给变量分配合适的内存空间 整型 作用:整型变量表示的是整数类型的数据 …

    Linux 2023年6月13日
    089
  • Linux用户和用户组

    Linux用户和用户组 1.添加新的用户 (用户ID从500开始,0-99系统管理级别、100-499系统预留) useradd 选项 用户名 参数说明 选项: -c commen…

    Linux 2023年6月11日
    082
  • 剑指offer计划21( 位运算简单)—java

    1.1、题目1 剑指 Offer 15. 二进制中1的个数 1.2、解法 通过判断每一位的与来识别1的数量。 1.3、代码 public class Solution { // y…

    Linux 2023年6月11日
    0116
  • 在.NET中体验GraphQL

    前言 以前需要提供Web服务接口的时候,除了标准的WEBAPI形式,还考虑了OData、GraphQL等形式,虽然实现思路上有很大的区别,但对使用方来说,都是将查询的主动权让渡给了…

    Linux 2023年6月6日
    0123
  • 编程入门之日志聚合系统

    (关心具体部署的同学,可以移步我的另外一篇《Centos部署Loki日志聚合系统 》https://www.cnblogs.com/uncleguo/p/15975647.html…

    Linux 2023年6月13日
    076
  • YUM简单入门

    1.制作YUM源先关闭相关安全设置,安装vsftpd 挂载到共享目录 对新增的安装包目录生成包的元数据(把包中依赖关系统计) 配置yum路径特性 生成yum 2.YUM命令简单使用…

    Linux 2023年6月7日
    093
  • ELK-企业级日志分析系统

    ELK 企业级日志分析系统 1.常见日志处理方式 rsyslog: Ryslog是一个强大而安全的日志处理系统。Rsylog通过多个物理或虚拟服务器在网络上接收日志,并监视不同服务…

    Linux 2023年6月13日
    080
  • neovim环境与vim简单使用

    Github仓库 neovim的配置 这里列出我自己使用的 init.vim,如果插件无法安装,请按照github仓库中给出的解决方法解决(手动clone安装即可)。参考了gith…

    Linux 2023年6月8日
    091
  • 所学自省

    本文是根据在大学的这几年接触的东西写的,给同为软件的,需要的同学参考参考,看看这几年自己在大学学了多少东西。 你学过的东西写了多少笔记?又记得多少?自己主动去设计一个项目来做的,有…

    Linux 2023年6月14日
    093
  • 清空Redis集群所有节点的数据工具

    FLUSHALL和FLUSHDB是单机命令,所以清空集群需要在所有Master节点上均执行一次。下载:https://github.com/eyjian/redis-tools/b…

    Linux 2023年5月28日
    096
  • 教你搞懂Jenkins安装部署!

    前言:请各大网友尊重本人原创知识分享,谨记本人博客: 南国以南i Jenkins介绍 Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作…

    Linux 2023年6月14日
    0115
  • 【Example】C++ std::thread 及 std::mutex

    与 Unix 下的 thread 不同的是,C++ 标准库当中的 std::thread 功能更加简单,可以支持跨平台特性。 因此在项目需要跨平台及对多线程简单应用情况下,应优先考…

    Linux 2023年6月13日
    060
  • shell handle

    !/bin/bash qinrui set -e commitId =” repoPath =” x1 =” if [-f changes15….

    Linux 2023年5月28日
    0104
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球