数据结构与算法(java)— HashMap和HashSet底层原理及实现

本文详细探讨了Java中HashMap和HashSet的数据结构与实现原理。HashMap基于哈希表实现,利用哈希函数进行快速定位,通过链地址法解决哈希冲突。在JDK1.8中引入了红黑树,优化了扩容过程。而HashSet内部依赖HashMap,存储元素作为HashMap的key,value则使用固定对象。
摘要由CSDN通过智能技术生成

数据结构与算法(java)— HashMap和HashSet底层原理及实现

1. HashMap内部原理及实现

1. 哈希表

哈希表(hash table)也叫散列表,是一种非常重要的数据结构,我们先来看一下其他数据结构的特点。
(1)数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n) 。
(2)链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。
我们可以发现,数组和链表几乎是两个极端,一个查找效率高,一个插入删除效率高,那么有没有一种数据结构融合两者的优点呢?没错,就是哈希表。
在哈希表中进行添加,删除,查找等操作,性能都非常高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),那么是如何做到的呢?首先,哈希表的主干为数组,例如我们要增加或查找某个元素,我们可以将当前元素通过某个函数映射到数组中的某个位置,通过数组下标直接定位即可。这个函数被称为哈希函数。
哈希函数的设计至关重要,好的哈希函数会尽可能地保证散列的地址分布均匀,但是再好的哈希函数也会出现冲突的情况,比如我们的两个元素通过哈希函数得到同一个存储地址,那么该如何解决呢?哈希冲突的解决方案有很多种,而HashMap采用了链地址法,就是数组+链表的方式,所有通过哈希函数得到同一地址的元素通过链表加在后面即可。

1.2 HashMap实现原理

HashMap是一个存储键值对的集合,允许存储null键和null值,线程不安全。HashTable不允许存储null值,线程安全,效率很差。
HashMap的主干是一个Node数组。Node是HashMap的基本组成单元,每一个Node包含一个key-value键值对。>

//HashMap的主干数组,可以看到就是一个Node数组,初始值为空数组,主干数组的长度一定是2的次幂
transient Node<K,V>[] table;

1.2.1 Node的内部结构

static class Node<K,V> implements Map.Entry<K,V> {
   
        final int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
        final K key;
        V value;
        Node<K,V> next;//存储指向下一个Entry的引用,单链表结构

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

这里写图片描述
1.2.2 HashMap中一些重要的字段

int threshold;             // 所能容纳的key-value对极限 
 final float loadFactor;    // 负载因子
 int modCount;  
 int size; 

Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改。
size这个字段其实很好理解,就是HashMap中实际存在的键值对数量,而modCount字段主要用来记录HashMap内部结构发生变化的次数,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法.

1.2.3 HashMap的hash方法

static final int hash(Object key) {
        int h;
        // h = key.hashCode() 为第一步 取hashCode值
        // h ^ (h >>> 16)  为第二步 高位参与运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    } 

//jdk1.7的源码,jdk1.8没有这个方法,jdk1.8直接使用这个算式而没有封装这个方法,但原理都一样
static int indexFor(int h, int length) {  
     return h & (length-1);  //第三步 取模运算
}

我们怎么把生成的hashcode变成数组索引均匀的分布在数组中呢?我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。 
但是,模运算的消耗还是比较大的,在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。我们看一下原理,n为table的长度

这里写图片描述

1.2.4 HashMap的put方法实现

思路:
    1.对key的hashCode()做hash,然后再计算index;
    2.如果没冲突直接放到数组里;
    3.如果冲突了,以链表的形式存在当前存在元素后;
    4.如果冲突导致链表过长(大于等于TREEIFY_THRESHOLD,默认为8),就把链表转换成红黑树;
    5.如果节点已经存在就替换old value(保证key的唯一性)
    6.如果数组满了(超过load factor*current capacity),就要resize扩容。
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    } 
    /**
    *生成hash的方法
    */
    static
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值