java数据结构当中的《Map和Set》(二)

一、核心概念:哈希表与哈希冲突

1. 哈希表(Hash Table)基础

哈希表是一种以空间换时间的数据结构,核心是通过「哈希函数」将键(Key)映射到数组的指定索引位置,实现平均 O(1) 时间复杂度的增删查改。

(1)核心公式:索引 = 哈希函数(Key) % 数组长度

(2)理想状态:每个 Key 都能通过哈希函数映射到唯一索引,无冲突、存取效率极高。

2. 哈希冲突(Hash Collision)

定义

当不同的 Key 通过哈希函数计算后,得到了相同的索引位置,就称为哈希冲突(哈希碰撞)。

(1)例:Key1="张三",哈希值 = 10;Key2="李四",哈希值 = 10;数组长度 = 8 → 索引 = 10%8=2,两者映射到同一索引,产生冲突。

(2)注意:哈希冲突无法完全避免(因为 Key 的范围远大于数组长度,根据鸽巢原理,必然存在冲突),只能 "避免" 或 "解决。

二、避免哈希冲突(重点:哈希函数设计 + 负载因子调节)

避免冲突的核心是让哈希值尽可能均匀分布,减少冲突概率,主要靠 2 个手段:

1. 哈希函数设计(核心)

哈希函数的目标:相同 Key 必须返回相同哈希值,不同 Key 尽可能返回不同哈希值。
常用设计原则 / 方法(满足 "均匀性")

关键优化(Java 中的实践)

Java 的 Object.hashCode() 是基础,但 HashMap 对其做了二次哈希(扰动函数),进一步打散哈希值:

java 复制代码
// HashMap 中的哈希函数(JDK8)
static final int hash(Object key) {
    int h;
    // 1. 取key的hashCode;2. 高16位与低16位异或,打散哈希值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

作用:减少 "高位不同、低位相同" 的 Key 产生冲突的概率(比如 Key1=0x12345678,Key2=0x98765678,低位相同,异或后哈希值差异更大)。

2. 负载因子调节(重点掌握)

定义

负载因子(Load Factor)= 哈希表中元素个数 / 数组长度。

例:数组长度 = 16,元素个数 = 8 → 负载因子 = 0.5。
核心作用

负载因子决定了哈希表的 "拥挤程度",是平衡「空间」和「冲突概率」的关键:

(1)负载因子过高 (如 1.0):数组满了才扩容,空间利用率高,但冲突概率剧增,存取效率退化为 O(n);

(2)负载因子过低 (如 0.2):冲突概率低,但数组空闲空间多,空间浪费严重。
Java HashMap 的实现(重点)

(1)默认负载因子:0.75(经过工程验证的最优值,平衡空间和冲突);

(2)扩容触发条件:元素个数 > 数组长度 × 负载因子 → 数组扩容为原来的 2 倍(必须是 2 的幂,配合位运算快速计算索引);

(3)例:数组长度 = 16,负载因子 = 0.75 → 元素个数≥12 时,扩容为 32。

三、解决哈希冲突(重点:开散列 / 哈希桶)

当冲突不可避免时,需要通过特定策略解决,主流有 2 种方式:

1. 闭散列(开放定址法)

定义

冲突发生时,在数组中寻找下一个空闲位置存放冲突元素,核心是 "在同一个数组里找位置"。

常用方法

(1)线性探测 :冲突后,依次往后找空闲位置(索引 + 1、+2...);

缺点:易产生 "聚集效应"(连续位置被占,后续冲突概率更高);

(2)二次探测 :冲突后,按索引 ±1²、±2²... 找空闲位置;

优点:缓解聚集效应;缺点:仍可能局部聚集;

(3)伪随机探测 :冲突后,按随机数序列找空闲位置。

缺点

数组满了无法扩容(或扩容成本高);

删除元素时需标记 "已删除",否则会破坏探测链;

冲突严重时,探测效率急剧下降。

2. 开散列(链地址法 / 哈希桶,重点掌握)

定义

数组的每个索引位置(称为 "桶")对应一个链表 / 红黑树 ,冲突的元素直接挂在同一个桶的链表 / 红黑树下,核心是 "数组 + 链表 / 红黑树"。
实现逻辑(以 Java HashMap为例)

  1. 数组初始长度为16,每个桶默认是null;
  2. Key计算哈希值后,通过(数组长度-1)&哈希值得到索引(替代取模,效率更高);
  3. 若索引位置为空,直接创建节点放入;
  4. 若索引位置已有节点(冲突),将新节点挂在链表尾部;
  5. 当链表长度≥8且数组长度≥64时,链表转为红黑树(降低查询复杂度至O(logn));
  6. 当红黑树节点数≤6时,转回链表(节省空间)。
    优势(为何成为主流)
    ●冲突处理简单,无聚集效应;
    •扩容成本低(仅需迁移部分桶);
    •删除元素无需标记,直接从链表/红黑树删除即可;
    ●Java、Python、C++等主流语言的哈希表均采用此方式。
    图解:
java 复制代码
数组索引:0    1        2              3    ...
        [ ]  [节点A]  [节点B→节点C→节点D]  [ ]

节点 B、C、D 哈希冲突,都映射到索引 2,挂在同一个链表下。

四、冲突严重时的解决办法

当哈希表冲突率过高(如负载因子超标、哈希函数设计差),需针对性解决:

  1. 优化哈希函数:重新设计哈希函数,让哈希值分布更均匀(如Java的扰动函数);
  2. 提高扩容频率:降低负载因子(如从0.75改为0.5),提前扩容,减少冲突;
  3. 调整数组长度:数组长度取质数(除留余数法)或2的幂(位运算高效),避免哈希值分布不均;
  4. 链表转红黑树:如Java HashMap,链表长度≥8 时转红黑树,将查询效率从O(n)降为O(logn);
  5. 分片哈希:将大哈希表拆分为多个小哈希表,分散冲突压力。

五、哈希表实现性能分析

性能关键影响因素

  1. 哈希函数均匀性:越均匀,冲突越少,性能越好;
  2. 负载因子:0.75是工程最优值,过高/过低都会影响性能;
  3. 冲突解决方式:开散列(红黑树)>开散列(链表)>闭散列;
  4. 数组扩容策略:2倍扩容+2的幂长度,兼顾效率和均匀性。

六、与 Java 类集的关系

  1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
  2. java 中使用的是哈希桶方式解决冲突的
  3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
  4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
相关推荐
青云计划11 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿11 小时前
Jsoniter(java版本)使用介绍
java·开发语言
2013编程爱好者11 小时前
【C++】树的基础
数据结构·二叉树··二叉树的遍历
NEXT0611 小时前
二叉搜索树(BST)
前端·数据结构·面试
化学在逃硬闯CS11 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
探路者继续奋斗12 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
消失的旧时光-194312 小时前
第十九课:为什么要引入消息队列?——异步系统设计思想
java·开发语言
A懿轩A13 小时前
【Java 基础编程】Java 面向对象入门:类与对象、构造器、this 关键字,小白也能写 OOP
java·开发语言
乐观勇敢坚强的老彭13 小时前
c++寒假营day03
java·开发语言·c++
biubiubiu070613 小时前
谷歌浏览器无法访问localhost:8080
java