0831(043天 集合框架07 哈希表)

0831(043天 集合框架07 哈希表)

每日一狗(田园犬西瓜瓜

在这里插入图片描述

集合框架07 哈希表

1. 哈希表

哈希表:一种以键值对存储数据的结构

1.1 Map<K, V>接口

public interface Map<K, V> {
	// k-键 v值  键不可重复
    int size(); // 键值对的个数
    boolean isEmpty(); // 判定是否包含该 key
    V put(K key, V value); // 键值对 放进容器
    V remove(Object key); // 删除指定键的
    Set<K> keySet(); // 获取键的集合,key不重复
    Collection<V> values(); // 获取值的容器,值可重复,无序
}

新增方法

// getOrDefault 获取指定范围的值
/ forEach
default void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action); // 判定非空
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch (IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
        action.accept(k, v); // 调用方法 传入参数
    }
}
//replaceAll 对容器中的v进行批量操作
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
    Objects.requireNonNull(function);
    for (Map.Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey(); // 获取键
            v = entry.getValue(); // 获取值
        } catch (IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }

        // ise thrown from function is not a cme.
        v = function.apply(k, v); // 调用传入的方法获取返回对象

        try {
            entry.setValue(v); // 将返回对象重新放回到v中
        } catch (IllegalStateException ise) {
            // this usually means the entry is no longer in the map.
            throw new ConcurrentModificationException(ise);
        }
    }
}
// putIfAbsent(待整理)
default V putIfAbsent(K key, V value) {
    V v = get(key); // 获取键对应的值
    if (v == null) {
        v = put(key, value); // 值为空时会覆盖数据
    }

    return v; // 返回 v 
}
///remove 按照键值对进行元素的删除,cas
default boolean remove(Object key, Object value) {
    Object curValue = get(key);
    if (!Objects.equals(curValue, value) || // 存储值和传入值不相同
        // 键值对不存在
        (curValue == null // 存储的值为空
         && !containsKey(key))) { // 键不存在
        return false;
    }
    remove(key); // 删除元素
    return true;
}
replace 和 remove大同小异
boolean replace(K key, V oldValue, V newValue); // 对老数据进行了判定
V replace(K key, V value); // 只对key和value进行了存在判定

1.2 问:一点点重点

hashMap显示顺序不是添加顺序,使用linkedHashMap就有序了

put会在添加数据的同时会将原始存储数据进行返回,但是放入的数据如果和原始存储的数据相同嘞。

V get(Object key);

get时,如果key不存在,会返回null

返回null:没有key;key存储的就是null

V remove(key);

根据键删除,key不存在不会抛出异常,返回值为null

key存在时返回对应的value

遍历map集合的三种方法

  • Iterator 迭代器
  • forEach 函数式方法
  • for(Object tmp : map.keySet())

遍历力度

  • map.keySet()
  • map.values()
  • map.entrySet()

**key是惟一的,需要对元素进行存在性判定时:**存在判定先调用hashCode,相同时在使用equals进行判定。所以一般需要对key中的元素的hashCode、equals方法重写。

value是可以重复的,在对其进行等值判定只调用equals方法,v中元素只需要对equals进行重写。

1.3 散列算法

Hash一般翻译为散列,也有直接音译为哈希的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。

所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。

消息摘要算法:md5,SHA-1等

哈希冲突:

不同的输入,相同的输出

解决办法:

开放地址法:你往后存(紧接着的后边)

链地址法:链表,挂到这个地址上的这个链子

以 key/value 的方式存储数据,采用拉链法综合了数组和链表结构。如果key已知则存取效率较高,但是删除慢,如果不知道key存取则慢,对存储空间使用不充分。最典型的实现是HashMap。

1.4 问:思想

数组+链表

哈希表的本质是一个数组,数组中每一个元素称为一个桶,桶中存放的是键值对

存储使用哈希冲突中的链地址法来进行数据存储

链子过长:对链子JDK8+后会使用红黑树进行存储

1.7之前采用Entry数组+链表实现。

1.8采用Node数组+单项链表+红黑树实现,在桶内的节点数量大于8时(并且全部节点数量 > 64 )会转换为红黑树,红黑树的数量小于6时会退化为单向链表

1.7之前采用头插法,会出现环形链,1.8采用尾插法,避免环形链

1.5 实现类

hashMap

负载因子: 负载因子越小哈希碰撞的概率越底,反之节约空间,哈希碰撞概率提升,检索效率底。一般在(0,1】

容积为啥老是2的倍数,因为扩容的时候会扩容两倍,扩容的时候需要移动的数据量就是一半。这样来进行移动代价比较小。

属性

transient int size; // 元素个数
transient int modCount; // 记录修改次数,多线程的操作的快死异常
transient Set<Map.Entry<K,V>> entrySet; // 并不是用于实际存储数据,主要用于针对entrySet和keySet两个视图提供支持
transient Node<K,V>[] table; // 存放数据的数组
/// 重要阈值
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 负载因子:限制当前容器中已经存储的数据量,超过设定及将数组进行扩容
static final int TREEIFY_THRESHOLD = 8;// 树化阈值:即链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;// 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表(即将链表转换成红黑树)否则,若桶内元素太多时,则直接扩容,而不是树形化
static final int UNTREEIFY_THRESHOLD = 6;// 桶的链表还原阈值:即红黑树转为链表的阈值,当在扩容resize时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将红黑树转换成链表

内部节点实现类

静态内部类用于实现Entry,HahMap中存放的key/value对就被封装为Node对象。其中key就是存放的键值,用于决定具体的存放位置;value是具体存放的数据,hash就是当前Node对象的hash值,next用于指向下一个Node节点(单向链表)

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 节点的哈希值
    final K key; // 键
    V value; // 值
    Node<K,V> next; // 下一个节点
    // 单向链表,单链节点数>8(且全链节点数>64时)会进行数组扩容同时转换未红黑树
}

构造器

/// 无参构造,延迟初始化
public HashMap() {
    // 延迟初始化 创建数组
    // 只是对loadFactor负载因子赋为默认值0.75,
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}
/// 指定容积大小,负载因子为默认的0.75
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/// 指定容积、负载因子
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0) // 容积非正判定
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY) // 容积最大值判定
        initialCapacity = MAXIMUM_CAPACITY;
    // 负载因子合法判定 Float.isNaN 是不是 不是一个浮点数 的判定
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor; // 
    this.threshold = tableSizeFor(initialCapacity); // 算一个合法的容积来用
}
19 Float.isNaN 是不是 不是一个浮点数 的判定
public static boolean isNaN(float v) {
    return (v != v);
}
23  获取合法的初始化容积,比cap刚好大一点的2^n的一个数,,eg:cap=5,return=8
static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); // 将-1向右位运算传入值前边0的个数-1
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

添加元素

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
/ 2 hash 计算键对应的哈希值,将高位和低位融合后返回
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : // 键不为空
    (h = key.hashCode()) // 计算键的hashCode
    ^ // 高位与底位进行异或运算,让高位和低位共同参与运算
        (h >>> 16); // 高位移动到低位
}

// 2 putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; 
    Node<K,V> p; 
    int n, // 容量
    i;
    // 1. 容器数组位空时进行初始化操作。延迟到这里进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. i = (n - 1) & hash算索引(计算的哈希值对容积进行求余),存储为null时创建node对象插入到对应的桶上
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        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 {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

问:为什么初始化容积值需要转换位2^n次方

  1. 扩容代价比较小
  2. 只有 2^n 次方才能使用位运算进行计算(效率嘎嘎高)
// 求余的实现方法 
// 一个数x对2^n进行求余,可以写成 x & (2^n-1)
2%8=2  2 & (8-1)  0010  & 0111 = 0010;
9%8=1  9 & (8-1)  1001 & 0111 = 0001;
15%8=7  15 & (8-1) 1111 & 0111=0111;

问:如何判断 环形链

判定key的重复性:遍历集合,将元素的key存放到set中,如果在下一个key已经在set中存在,则出现环形链表

问:浅谈一下HashMap的构造器

该构造器一共可指定两个参数,int initialCapacity初始化容积(默认DEFAULT_INITIAL_CAPACITY=16), float loadFactor加载因子(默认DEFAULT_LOAD_FACTOR0.75),值越大则存储数据越多,hash冲突的概率越高;值越小则越浪费空间,但是hash冲突的概率越低。

为了减少hash冲突,引入了加载因子,表示了当前容器只能存储最大容积的百分之多少,超过了就会对容器进行扩容。默认容器容积为16,加载因子为0.75,也就是说这个容器在存储到16*0.75=12时,在添加元素就会对容积进行扩容。

在构造器中并没有创建任何用于存储数据的集合—延迟加载,第一次存储数据时才进行空间分配。

扩展小芝士

  • 对象的equals方法判定相等,hashCode值必须相等
  • 位运算: >>带符号右移(空位置会使用符号位进行补全),>>> 不带符号右移动,空位置使用0进行补全
    • 异或运算:不一样才是真
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值