Java学习笔记: HashMap 和 HashSet

本文详细介绍了Java中的HashMap和HashSet数据结构及常用方法。HashMap存储结构基于哈希桶,允许键值对的唯一性,负载因子默认为0.75,当哈希冲突时可能会转换为树状结构。HashSet基于HashMap实现,存储唯一对象,内部元素顺序不保证。哈希表是一种高效的数据结构,通过哈希函数快速定位元素,但在哈希冲突时需要采取解决策略,如闭散列法和开散列法。
摘要由CSDN通过智能技术生成

📒数据结构专栏简介: 内容主要以介绍数据结构为主, 希望读者看了之后能有所收获🉐
👷🏻‍作者也是一名刚上路的小白coder🛵, 欢迎各位评论, 批评指正或私信交流👁‍🗨~~



关于Map接口和Set接口在上一篇关于TreeMap文章中已经介绍过, 这里不再赘述, 详见: http://t.csdn.cn/L44vx

HashMap

HashMap是Map接口下的一个实现子类, HashMapTreeMap一样, 同样存储的是<Key, Value>的键值对, 与 TreeMap 相同的是, HashMap中的Key 也是唯一的, 当有相同的Key的<Key, Value>, 会更新之前的Value(若Key已存在)

存储结构

HashMap的存储容器通常是一个哈希桶, 在存储的元素过多时, 容器的结构会转换成一个类似于TreeMap的树状结构. 树中的元素排序依据主要是哈希值(如果元素对应的类实现了Comparable接口则基于compareTo方法进行比较排序)

我们可以观察HashMap的源码进行学习

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

哈希表的初始容量是16

static final int MAXIMUM_CAPACITY = 1 << 30;

最大容量是 1 << 30(1073741824)

/** The bin count threshold for using a tree rather than list for a bin.  Bins are converted to trees when adding an element to a bin with at least this many nodes. The value must be greater than 2 and should be at least 8 to mesh with assumptions in tree removal about conversion back to plain bins upon shrinkage. */
static final int TREEIFY_THRESHOLD = 8;

树化的阈值是8, 该阈值必须大于2, 且最小应该是8, 只有这样才匹配树容器和普通容器间去树化转换的收缩前提

/** The bin count threshold for untreeifying a (split) bin during a resize operation. Should be less than TREEIFY_THRESHOLD, and at most 6 to mesh with shrinkage detection under removal. */
static final int UNTREEIFY_THRESHOLD = 6;

解除树化的阈值是6, 该阈值应该要小于树化阈值, 且也要与收缩检测相匹配

 /** The smallest table capacity for which bins may be treeified. (Otherwise the table is resized if too many nodes in a bin.) Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts between resizing and treeification thresholds. */
static final int MIN_TREEIFY_CAPACITY = 64;

可能树化的最小容量是64, 而哈希表的扩容大小最小值至少为4*树化的最小容量

/** The load factor used when none specified in constructor.*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

默认的负载因子是0.75

负载因子定义:

哈希表的负载因子 α = 表 中 元 素 个 数 表 的 长 度 ( 容 量 ) α = \frac {表中元素个数}{表的长度(容量)} α=()

当负载因子过大时, 插入新元素时就容易发生冲突

常用方法

构造方法

方法简述
HashMap()构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap
HashMap(int initialCapacity)构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
HashMap(int initialCapacity, float loadFactor)构造一个带指定初始容量和负载因子的空 HashMap
HashMap(Map<? extends K,? extends V> m)构造一个映射关系与指定 Map 相同的新 HashMap

方法

方法简述
void clear()从map中移除所有映射关系
boolean containsKey(Object key)如果map中包含对于指定键的映射关系,则返回 true
boolean containsValue(Object value)如果map中将一个或多个键映射到指定值,则返回 true
Set<Map.Entry<K,V>> entrySet()返回map中所包含的映射关系的 Set 视图
V get(Object key)返回指定键所映射的值;如果对于该键来说,map中不包含任何映射关系,则返回 null
V put(K key, V value)在此map中关联指定值与指定键
V remove(Object key)从此映射中移除指定键的映射关系(如果存在)
boolean isEmpty()如果map中不包含键-值映射关系,则返回 true
int size()返回map中的键-值映射关系数

Map.Entry<K,V>是一个接口, 详情可见TreeMap介绍文章

HashSet介绍

HashSet实现了Set接口, 存储的是Key, Key是唯一的

存储结构

/**
 * This class implements the <tt>Set</tt> interface, backed by a hash table
 * (actually a <tt>HashMap</tt> instance).  It makes no guarantees as to the
 * iteration order of the set; in particular, it does not guarantee that the
 * order will remain constant over time.  This class permits the <tt>null</tt>
 * element. */

由源码的注释我们可以知道HashSet是借HashMap实现的, HashSet中的元素排序顺序是不能保证的, 且允许储存null

常用方法

构造方法

方法简述
HashSet()构造一个新的空 set,其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75
HashSet(Collection<? extends E> c)构造一个包含指定 collection 中的元素的新 Set
HashSet(int initialCapacity)构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和默认的负载因子
HashSet(int initialCapacity, float loadFactor)构造一个新的空 Set,其底层 HashMap 实例具有指定的初始容量和指定的负载因子

方法

方法简述
boolean add(E e)如果 set 中尚未包含指定元素,则添加指定元素
void clear()从 set 中移除所有元素
boolean contains(Object o)如果 set 中包含指定元素,则返回 true
boolean isEmpty()如果 set 中不包含任何元素,则返回 true
boolean remove(Object o)如果指定元素存在于此 set 中,则将其移除
int size()返回此 set 中的元素的数量(set 的容量)

哈希表

一般的数据结构如列表, 链表等进行搜索操作时是需要通过遍历整个表(O(n))才能完成的, 而平衡树的时间复杂度也是与树的高度有关(O(logn)), 而我们想要的是一种更高效, 更理想的搜索模式, 就如狙击枪一般指哪打哪, 达到近似(O(1))的效率, 而Hash Table正是为此而生的.

哈希表(又称散列表)中通过构造元素中关键码和元素的存储位置一一对应的映射关系, 使其简化搜索的过程

基本操作(CURD)

  • 插入(Create) :

    计算出元素对应的哈希值, 依据哈希值将元素存储到对应位置

  • 搜索(Read):

    根据搜索元素中的关键字计算出哈希值, 将对应的存储的元素的哈希值进行比较, 相同则返回其引用

  • 更新(Update):

    与搜索类似, 在搜索的基础下, 对应哈希值相同则更新

  • 删除(Delete)

    与搜索类似, 在搜索的基础下, 对应哈希值相同则将该引用置空

哈希冲突

当哈希表中的元素越来越多时, 总会发生的情况: 有两个或多个元素计算出的哈希值而指向同一块存储空间, 这就是发生了哈希冲突(哈希碰撞), 发生的碰撞越多自然也会降低效率, 这是我们不愿看到的

哈希函数设计

为了降低发生哈希冲突的频率, 在哈希函数设计上我们可以下一些功夫, 常用的哈希函数的设计有:直接定制法, 除留余数法, 平方取中法, 折叠法, 数学分析法, 在这就简单介绍两种常用的方法 直接定制法 和 除留余数法, 其他方法读者若有兴趣可以自行查找资料, 优秀哈希函数设计可以有效降低哈希冲突发生的频率, 但无法完全避免哈希冲突的发生

直接定制法

取元素中的关键字为变量, 通过线性函数运算得到哈希值(散列地址), 如 :

Hash (Key) = A * Key + B

这种办法的优点是比较简单, 适用于已知关键字范围, 计算出的地址要数量不大且分布比较连续

除留余数法

同理取元素中的关键字为变量, 设哈希表的容量为max, 取最接近或者等于max的质数p(p ≤ m)作为除数, 如 :

Hash (Key) = Key % p

这种方法的优缺点与上述方法相似

负载因子的调节

哈希冲突发生的频率与负载因子也有关, 当哈希表中存储的元素越多, 负载因子越大, 发生碰撞的概率也就越高. 所以要对负载因子进行调节, 从上述负载因子的定义来看, 调节负载因子可以改变哈希表的容量

要注意的是调节哈希表容量时, 若设计的哈希函数与哈希表的容量有关, 要将其中存储的元素重新进行散列地址的计算重新存储

哈希冲突的解决办法

闭散列法

当发生哈希冲突时, 若哈希表的存储元素个数还未达到最大, 我们总能找到空位存放发生冲突的元素

  • 线性探测: 当发生哈希冲突时, 依次往后寻找到空位即可

线性探测

如图上所示, 假设哈希函数是Hash(Key) = Key % 8, 要插入Key 为 1 的元素, Index 为 1 处已经存有 Key 为 9 的元素, 当我们想存入Key为 1 的元素时, 就会发生冲突, 此时依次往后寻找到 Index 为 2 时有空位, 插入即可

  • 二次探测: 当发生哈希冲突时, 寻找下一个位置的方法为 H a s h ( K e y ) = ( H 0 + i 2 ) Hash(Key) = (H_{0} + i^{2} ) Hash(Key)=(H0+i2)% m ( H 0 H_{0} H0是初次计算出的哈希值, 式中也可以将 H 0 H_{0} H0 i 2 i^{2} i2 相减)

二次探测

如上图所示, 依然假设哈希函数是Hash(Key) = Key % 8, 要插入Key 为 17 的元素, Index 为 1 处已经存有元素, 通过上述公式计算得下一个 Index 为 2 处, 也存有元素, 则计算第二次得Index 为 5, 为空可插入

开散列法

开散列法则是利用类似链表的结构处理冲突, 创建一个存储哈希桶引用的容器, 当发生冲突时, 只需用头插法处理即可

开散列

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值