浅谈hashmap(上)

微信原文-排版更佳

每日一题 关注公众号:Troye Jacobs

​1).前篇

Map:双列数据,存储key-vaLue对的数据

HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value

LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap.

TreeMap;保证按照添加的key-vaLue对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序

HashtableI作为古老的实现类;线程安全的,效率低(锁的是整段代码,后面会介绍ConcurrentHashMap–分段锁);不能存储null的key和value

HashMap的底层:数组+链表(jdk7及之前)

数组+链表+红黑树(jdk 8)

2).hashmap常量

//hashMap默认容量-须为2的次方幂

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//hashMap最大容量 2的30次方

static final int MAXIMUM_CAPACITY = 1 << 30;

//加载因子 散列表扩容时用的

static final float DEFAULT_LOAD_FACTOR = 0.75f;

//Bucket中链表长度大于该默认值,可能转化为红黑树

static final int TREEIFY_THRESHOLD = 8;

//Bucket中红黑树存储的Node小于该默认值,转化为链表

static final int UNTREEIFY_THRESHOLD = 6;

//当Bucket中链表长度大于8且散列表中元素个数大于64时转化为红黑树树

static final int MIN_TREEIFY_CAPACITY = 64;

/**

table:存储元素的数组,总是2的n次幂

entrySet:存储具体元素的集
size: HashMap中存储的键值对的数量
modCount:HashMap扩容和结构改变的次数。
threshold:扩容的临界值,=容量*填充因子
loadFactor:填充因子
**/
3).什么是哈希

核心理论:Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。

Hash的特点:

1.从hash值不可以反向推导出原始的数据

2.输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值

3.哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值

4.hash算法的冲突概率要小

由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间。

根据抽屉原理,一定会存在不同的输入被映射成相同输出的情况。

这一现象就是我们所说的“抽屉原理”。

抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。

4)put执行过程

map.put(“troye”,“jacobs”);

调用key对象的hashcode()方法计算key-"troye"的hash

经过扰动函数使其hash值更散列(调用key对象的hashcode方法计算出来hash值,将 Hash 值的高 16 位右移并与原 Hash 值取异或运算(^),混合高 16 位和低 16 位的值,得到一个更加散列的低 16 位的 Hash 值)

进入putVal方法

判断散列表是否为空 也就是 put方法第一次调用才初始化hashMap的存储结构Node<k,v>[] table 散列表 初始为数组长度16

调用(n - 1) & hash 散列表数组长度-1 与 hash值得到将要把元素插到哪里的数组下标

判断数组该位置是否为空

如果为空 新创建一个结点直接插入 tab[i] = newNode(hash, key, value, null);

如果插入位置已经有值了tab[i]!=null;

如果桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作

否则就需要往改结点后添加元素

判断是否为树结构,若树结构按照树结构插入结点方法插入

不是树结构则按照链式结构插入

遍历改链表,判断是否有与你要插入的key一致的node

如果没有则将结点插入到该链表末尾(1.8尾插法 1.7头插法),并判断插入后是否达到树化条件(链表长度>=8 进入treeifyBin(tab, hash);进入该方法还需要判断当前数组长度>=64才能树化,如果<64则扩容)

找到相同元素则需要替换

完成插入操作了 ++modCount(散列表结构结构被修改的次数–替换Node元素的value不算)

size自增,如果自增后的值大于扩容阈值,则触发扩容resize();

//代码跟踪
Map<String,String> map=new HashMap<>();
map.put(“troye”,“jacobs”);
//以下是jdk1.8源代码
//真正执行存值操作的是 putVal操作
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

//hash扰动函数-使其hash值更散列
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


class Object{
....
public native int hashCode();
.....
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
     //tab:引用当前hashMap的散列表
    //p:表示当前散列表的元素
    //n:表示散列表数组的长度
    //i:表示路由寻址结果
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    //延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的最耗费内存的散列表
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
        
   //最简单的一种情况:寻址找到的桶位刚好是null,这个时候,直接将当前k-v=>node 扔进去就可以了
   //i=(n-1)& hash  将散列表数组长度长度-1 与(&) hash值得到插入元素的下标
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
      //
        Node<K,V> e; K k;
        
        //表示桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
         //判断是否为树结构(红黑树)
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        
        //这就是链式结构操作--链表的头元素与我们要插入的key不一致。
            for (int binCount = 0; ; ++binCount) {
            
            //条件成立的话,说明迭代到最后一个元素了,也没找到一个与你要插入的key一致的node
                if ((e = p.next) == null) {
                //将元素加入到当前链表的末尾
                    p.next = newNode(hash, key, value, null);
                //判断是否需要树化 --树化阈值-1(binCount是从0开始的)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                
    //e.hash是p.next 它的hash与要插入元素hash相等且key值也相同,说明找到了相同key的node元素,需要进行替换操作
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        //e不等于null,条件成立说明,找到了一个与你插入元素key完全一致的数据,需要进行替换
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    //modCount:表示散列表结构被修改的次数,替换Node元素的value不计数
    ++modCount;
    //插入新元素,size自增,如果自增后的值大于扩容阈值,则触发扩容。|
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

5)resize()扩容操作

对table进行初始化或者扩容。

  • 如果table为null,则对table进行初始化

  • 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。

final Node<K,V>[] resize() {
//oldTab引用原table数组
Node<K,V>[] oldTab = table;
//如果原table数组为null,设置原table长度oldCap=0,否则就是.length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//将原扩容阈值赋值给oldTre
int oldThr = threshold;
//定义新数组长度newCap 扩容阈值newThr
int newCap, newThr = 0;
//如果原数组长度大于0
if (oldCap > 0) {
//两个情况
//1.超过了数组最大长度,将扩容阈值设置成Integer.Max_value,并将原来数组返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//2.否则没有超过最大长度 则翻倍扩容(oldCap<<1) 如果原数组长度翻倍后没有超过数组最大长度
//再加上原来数组长度是>=数组默认初始化长度的话 就 将扩容阈值也翻倍赋值给新扩容阈值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//翻倍扩容
newThr = oldThr << 1; // double threshold
}
// oldCap == 0(说明hashmap中的散列表是null)且oldThr > 0 ;下面几种情况都会出现oldCap == 0,oldThr > 0
// 1.public HashMap(int initialCapacity);
// 2.public HashMap(Map<? extends K, ? extends V> m);并且这个map有数据
// 3.public HashMap(int initialCapacity, float loadFactor);
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//否则 就赋值默认值 数组长度=16 扩容阈值16*0.75=12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({“rawtypes”,“unchecked”})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//原数组不为空 则需要将原散列表的值经过重新哈希计算放入扩容后的数组
if (oldTab != null) {
//遍历数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果数组不为空 即头节点不为空
if ((e = oldTab[j]) != null) {
//上面已经将原来数组里头节点赋值给了临时变量e 下面就将原数组那个位置值null,便于Jvm回收
oldTab[j] = null;
//头节点不为空 还要看后面还有没有结点
if (e.next == null)
//如果后面没有结点 则直接根据路径寻址算法计算出存入位置放入
newTab[e.hash & (newCap - 1)] = e;
//树化操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order

                 // 低位链表:存放 扩容之后数组下标的位置与当前数组下标位置一致 的元素
                  // 高位链表:存放在扩容之后的数组下标的位置为当前数组下标位置+ 扩容之前数组长度的元素
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        //下一个结点
                        next = e.next;
                        //根据这个拆链(高位链 低位链)
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //组装 将高链表 低位链表插入新数组中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

6)大厂面试题

(先自己根据题目回答 不清楚再看我的理解–下面附属答案只能是我的理解,有错望纠正,一起进步!)

1.说说你对hash算法的理解
追问:hash算法任意长度的输入 转化为了 固定长度的输出,会不会有问题呢?
追问:hash冲突能避免么?
2.你认为好的hash算法,应该考虑点有哪些呢?
3.HashMap中存储数据的结构是什么样的呢?
4.创建HashMap时,不指定散列表数组长度,初始长度是多少呢?
追问:散列表是new HashMap() 时创建的么?
5.默认负载因子是多少呢,并且这个负载因子有什么作用?
6.链表转化为红黑树,需要达到什么条件呢?
7.Node对象内部的hash字段,这个hash值是key对象的hashcode()返回值么?
追问:这个hash值是怎么得到呢?
追问:hash字段为什么采用高低位异或?
8.HashMap put 写数据的具体流程,尽可能的详细点!

9.JDK8 hashmap为什么引入红黑树?解决什么问题?
追问:为什么hash冲突后性能变低了?【送分题】
10.hashmap 什么情况下会触发扩容呢?
追问:触发扩容后,会扩容多大呢?算法是什么?
追问:为什么采用位移运算,不是直接*2?
11.hashmap扩容后,老表的数据怎么迁移到扩容后的表的呢?
12.hashmap扩容后,迁移数据发现该slot是颗红黑树,怎么处理呢?

13.红黑树的写入操作,是怎么找到父节点的,找父节点流程?
14.TreeNode数据结构,简单说下。
15.红黑树的原则有哪些呢?

附属:

一种将任意长度的输入转为为固定长度的输出的映射规则

会造成哈希冲突

hash冲突不可避免 只能说减少冲突(可以用再哈希,链地址法等等)

好的hash算法 效率高 每个微小的变化都应该得到不同的hash值 不可反向推导 冲突小

jdk1.7及以前是数组+链表 jdk1.8是数组+链表+红黑树

默认初始长度16

不是在new HashMap()创建的 当第一次调用put方法时 执行putVal时才创建散列表

负载因子默认0.75 在计算扩容阈值时用

当链表长度>=8 且 数组长度>=64时树化

Node对象里面的hash值并不是直接key.hashcode得到 还要经过扰动函数 将 Hash 值的高 16 位右移并与原 Hash 值取异或运算(^),混合高 16 位和低 16 位的值,得到一个更加散列的低 16 位的 Hash 值

上一问已回答

高低位异或是为了让hash值更加散列

put流程:上文有,这里不再介绍

引入红黑树我认为是这样 当产生hash冲突时会形成链表 当数据多了冲突多了 链表越来越长 造成链化 此时查询将特别耗时 本来时间复杂度为O(1) 结构可能达到 O(n)

链化kk

当达到扩容阈值时

翻倍扩容

这个我也没搞懂???不一样的吗

如果后面没有结点 则直接根据路径寻址算法计算出存入位置放入

如果是树结点则进行红黑树操作(后续再讲)

将链表根据e.hash & oldCap拆分为高低位链表低位链表:存放 扩容之后数组下标的位置与当前数组下标位置一致 的元素;高位链表:存放在扩容之后的数组下标的位置为当前数组下标位置+ 扩容之前数组长度的元素

组合高低位链表

红黑树下篇讲

别忘关注点赞噢!
https://mp.weixin.qq.com/s/RbCZC9EmSLppnloe5Hy2aw
关注公众号:Troye Jacobs

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值