首页 > 其他分享 >数据结构(HashMap)

数据结构(HashMap)

时间:2024-04-15 16:12:39浏览次数:25  
标签:hash HashMap 数组 链表 key 数据结构 LinkedHashMap

散列表也叫哈希表,是一种通过键值对直接访问的数据结构,哈希能快速的插入、删除、查找操作

HashMap的结构

hashMap采用的链地址法,主要采用数据+链表(1.8 后转红黑树)的数据结构。

HashMap 的存储数据

HashMap 是使用哈希表来存储数据的。哈希表为了解决冲突,一般有两种方案:开放地址法链地址法

开放地址法:哈希完后如果有冲突,则按照某种规则找到空位插入

HashMap 采用的便是链地址法,即在数组的每个索引处都是一个链表结构,这样就可以有效解决 hash 冲突。

当两个 key 的 hash 值相同时,则会将他们至于数组的同一个位置处,并以链表的形式呈现。

我们都知道数组的时间复杂度的O(1),所以我们在存储的时候尽量将数据散列到数组中,即减少哈希碰撞。提升查询效率,所以一个好的hash算法是提升效率的关键

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

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销

hash运算的原理不必深究,关乎数学和散列学原理

hashMap的默认参数

// 默认的初始容量为 16 (PS:aka 应该是 as know as)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 最大容量(容量不够时需要扩容)
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表长度为 8 的时候会转为红黑树
static final int TREEIFY_THRESHOLD = 8;

// 长度为 6 的时候会从红黑树转为链表
static final int UNTREEIFY_THRESHOLD = 6;

// 只有桶内数据量大于 64 的时候才会允许转红黑树
static final int MIN_TREEIFY_CAPACITY = 64;

这些默认参数也是经过计算后和框架设计的最优值。比如默认容量,2的n次幂,链表转红黑树的阈值这些(短链表效率会比红黑树要高)

重要参数

// Map 中存储的数据量,即 key-value 键值对的数量
transient int size;

// HashMap 内部结构发生变化的次数,即新增、删除数据的时候都会记录,
// 注意:修改某个 key 的值,并不会改变这个 modCount
transient int modCount;

// 重点,代表最多能容纳的数据量
// 即最多能容纳的 key-value 键值对的数量
int threshold;

// 负载因子,默认为 0.75
// 注意,这个值是可以大于 1 的
final float loadFactor;

其中有两个参数需要注意一下,一个是 threshold,还有一个是 loadFactor

threshold 代表最多能容纳的 Node 数量,一般 threshold = length * loadFactor,也就是说要想 HashMap 能够存储更多的数据(即获得较大的 threshold),有两种方案,一种是扩容(即增大数组长度 length),另一种便是增大负载因子。

threshold 和数组长度不是一回事哦

0.75 这个默认的负载因子的值是基于时间和空间考虑而得的一个比较平衡的点,所以负载因子我们一般不去调整,除非有特殊的需求:

1、比如 以空间换时间,意思是如果内存比较大,并且需要有较高的存取效率,则可以适当降低负载因子,这样做的话,就会减小哈希碰撞的概率。

2、再比如 以时间换空间,意思是如果内存比较小,并且接受适当减小存取效率,则可以适当调大负载因子,哪怕大于 1,这样做的话,就会增大哈希碰撞的概率。

HashMap的put方法

从流程图上其实很清晰的知道HashMap存储的整个流程,先判断数组节点是否包含值,如果不包含则resize()初始化扩容,否则判断key值,如果存在则生成链表的方式,判断阈值转红黑树操作

重点还是如果高效率的计算key和resize()方法

jdk8通过 (p = tab[i = (n - 1) & hash] 的方式计算下标,而jdk7通过取模的方式 h & (length-1),在计算机运算中 与运算效率比取模运算高

HashMap 的长度 length 始终是 2 的幂次方,这个是关键,所以才会有这种结果,简单分析见下图:

![hashMap &运算](D:\Users\72127302\Desktop\资料\doc\hashMap &运算.png)

HashMap 的扩容机制

什么时候需要扩容?

当 HashMap 中的元素个数超过数组长度 loadFactor(负载因子)时,就会进行数组扩容,loadFactor 的默认值是 0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为 16,那么当 HashMap 中的元素个数超过 16×0.75=12(这个值就是阈值)的时候,就把数组的大小扩展为 2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预知元素的个数能够有效的提高 HashMap 的性能。

怎么进行扩容的?

HashMap 在进行扩容时使用 resize() 方法,计算 table 数组的新容量和 Node 在新数组中的新位置,将旧数组中的值复制到新数组中,从而实现自动扩容。因为每次扩容都是翻倍,与原来计算的 (n-1)&hash 的结果相比,只是多了一个 bit 位,所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。

因此,我们在扩充 HashMap 的时候,不需要重新计算 hash,只需要看看原来 hash 值新增的那个 bit 是 1 还是 0 就可以了,是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap(原位置+旧容量)”。这里不再详细赘述,可以看看下图为 16 扩充为 32 的 resize 示意图:

总结

整体来说HashMap的源码较少,主要还是要理解hash表的设计思想。利用2的n次幂来做数组容量,方便后续扩容和hash运算,提升性能。hash碰撞采用链地址法和红黑树的方式来提升查询性能。

当整体容量 > threshold 则需要扩容,避碰出现大量hash碰撞,导致性能下降;

Q&A

1、既然HashMap是通过hash运算来赋值的,这肯定是无序的,那LinkedHashMap是如何保证顺序的

  • LinkedHashMap 内部维护了一个双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题
  • LinkedHashMap 元素的访问顺序也提供了相关支持,也就是我们常说的 LRU(最近最少使用)原则
/**
* 该引用始终指向双向链表的头部
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* 该引用始终指向双向链表的尾部
*/
transient LinkedHashMap.Entry<K,V> tail;


static class Entry<K,V> extends HashMap.Node<K,V> {
   Entry<K,V> before, after;
   Entry(int hash, K key, V value, Node<K,V> next) {
       super(hash, key, value, next);
   }
}

内部维护了两个节点

// HashMap newNode 中实现
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

// LinkedHashMap newNode 的实现
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 将 Entry 接在双向链表的尾部
    linkNodeLast(p);
    return p;
}

在newNode时将Entry 接在双向链表的尾部,在iterator() 遍历时优先从head节点取值遍历

https://juejin.cn/post/6844903590159450120

2、LinkedHashMap和TreeMap的区别

  1. 内部数据结构:
    • TreeMap使用红黑树(Red-Black Tree)来存储键值对,因此它内部的键值对是有序的,按照键的自然顺序或者自定义比较器的顺序进行排序。
    • LinkedHashMap使用双向链表来维护插入顺序或者访问顺序,因此它可以保持键值对的插入顺序或者访问顺序。
  2. 性能:
    • TreeMap的插入、删除和查找操作的时间复杂度为O(log n),因为它使用红黑树来维护数据结构。
    • LinkedHashMap的插入、删除和查找操作的时间复杂度为O(1),因为它使用了哈希表来存储键值对,同时又通过双向链表来维护插入顺序或者访问顺序。
  3. 内存占用:
    • TreeMap在内存使用方面通常比LinkedHashMap更高,因为红黑树的结构相对复杂。
    • LinkedHashMap在内存使用方面通常比TreeMap更低,因为它只需要额外存储双向链表的指针。
  4. 应用场景:
    • 如果需要按照键的自然顺序或者自定义顺序进行遍历和操作,可以选择TreeMap。
    • 如果需要保持插入顺序或者访问顺序,可以选择LinkedHashMap。

3、entrySet() 的获取元素原理

public Set<Map.Entry<K,V>> entrySet() {
	Set<Map.Entry<K,V>> es;
	return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

当我们foreach遍历map时,其实调用EntrySet的iterator()迭代器方法

public final Iterator<Map.Entry<K,V>> iterator() {
    return new EntryIterator();
}

https://www.cnblogs.com/kukufan/p/15937813.html

3、HashMap是线程不安全的,体现在哪

Jdk7 链地址法采用的头插法,在多线程扩容中会性能环形链表

Jdk8 采用的尾插法避免了环形链表的问题,但还是会在插值时会被覆盖

4、JDK7和JDK8的实现区别

  • 计算hash的方式不同
  • resize()的方式不同
  • JDK7只有链表,JDK8引入了红黑树
  • JDK7采用头插法,JDK8采用尾插法
  • JDK7 节点对象时Entry,JDK8改为Node

源码了解更多

https://juejin.cn/post/6844904111817637901

https://tech.meituan.com/2016/06/24/java-hashmap.html

https://xie.infoq.cn/article/01538faefd4816128ede4212a

标签:hash,HashMap,数组,链表,key,数据结构,LinkedHashMap
From: https://www.cnblogs.com/luojw/p/18136178

相关文章

  • C++数据结构和pb数据结构的转换
    1.C++topb1.1map嵌套对象结构 //pb数据结构messageInner{repeatedstringcodes=1;map<string,string>ext=2;};messageOuter{map<int32,Inner>uint2Inner=1;map<string,string>ext=2;};赋值代码:Outerreq;req.mu......
  • 数据结构:时间复杂度
    时间复杂度:表示算法执行所需的大致时间,记作O(N)。一、当执行次数为常量时记作O(1)。二、执行次数只保留最高阶项例:已知时间复杂度的函数式为F(N)=N^2+2N+10,N无穷大时,2N和10对函数影响的无穷小,可以忽略不计,因此只取N^2为执行次数记作O(N^2)。三、如果最高阶存在且不为1,则......
  • 数据结构分类
    数据结构分类逻辑结构集合结构:只是属于一个集合线性结构:一对一的关系树形结构:图形结构:多对多物理结构顺序存储:把数据存放在地址连续的存储单元里,数据间的逻辑关系和物理关系是一致的例如数组链式存储:把数据元素存放在任意的存储单元里,这组存储单元可以......
  • 数据结构-树
    数据结构-树1.定义:树是一种分层数据结构,由节点组成。每棵树有一个根节点,每个节点除了根节点外都恰有一个父节点,并可能有多个子节点。它是一种非线性数据结构,用于表示具有层级关系的数据。classTreeNode:def__init__(self,data):self.data=datasel......
  • 数据结构-图
    数据结构-图1.定义:图是一种由节点(或称为顶点)和连接这些节点的边组成的数据结构。图可以用来表示任何二元关系,比如路线、网络、状态转换等。在Python中,可以使用邻接表或邻接矩阵来表示图classGraph:def__init__(self):self.graph={}2.类型:图分为有向......
  • 数据结构-队列
    数据结构-队列1.定义:队列是一种遵循先进先出(FIFO,FirstInFirstOut)原则的线性数据结构。在队列中,元素从一端添加(队尾),从另一端移除(队头)。classQueue:def__init__(self):self.items=[]主要操作:队列的主要操作包括enqueue(向队尾添加元素)、dequeue(从队头......
  • 数据结构-栈
    数据结构-栈1.定义:栈是一种只能在一端进行插入和删除操作的线性数据结构。它遵循后进先出(LIFO,LastInFirstOut)的原则,即最后插入的元素将是第一个被移除的classStack:def__init__(self):self.items=[]defis_empty(self):returnself.ite......
  • 数据结构-堆
    数据结构-堆1.定义:堆是一种特殊的完全二叉树。在堆中,所有的父节点都满足特定的顺序关系(大于或小于)与它们的子节点。这种属性使得堆在处理优先级队列和排序操作时非常有效。2.类型:常见的两种类型的堆是最大堆和最小堆。在最大堆中,每个父节点的值都大于或等于其子节点的值;而在......
  • 数据结构-哈希表
    数据结构-哈希表1.定义:哈希表(也称为散列表)是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。它通过将键映射到表中一个位置来访问记录,从而加快访问速度。#创建一个空字典hash_table={}#向哈希表中添加键-值对hash_table['apple']=10hash_table['banana'......
  • 数据结构-链表
    数据结构-链表1.链表的基本概念:链表是一种常见的数据结构,它由一系列节点组成,每个节点包含数据和指向列表中下一个节点的引用(或指针)。2.单向链表:单向链表的每个节点只有一个指向下一个节点的链接。这种结构使得遍历只能单向进行,从头节点开始到尾节点结束。classNode:d......