day-2-4-4

哈希表

构造器
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; //给属性loadFactor负载因子赋默认值0.75
        //并没有对数组进行初始化操作。采用延迟初始化的策略可以节约空间
}
    
//当指定初始化容积时,调用另外的构造器进行HashMap的初始化,采用默认的负载因子0.75
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
    
//2个参数的构造器,参数1初始化容积大小,参数2负载因子
public HashMap(int initialCapacity, float loadFactor) {
//对初始化容积值进行合法性验证,如果<0则抛出异常
if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY; 
    if (loadFactor <= 0 || Float.isNaN(loadFactor))  负载因子应该在(0,1]之间的浮点数
    	//负载因子越小,哈希碰撞的概率越低,也就意味着链表长度越小,但是浪费空间;如果值越大,则节约空间,但是哈希碰撞概论提高,所以一般建议值在(0,1]之间
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    	this.loadFactor = loadFactor;   //接收合法的负载因子值
    	this.threshold = tableSizeFor(initialCapacity);  //注意这里存储的是阈值。并不是直接使用传入的参数作为初始化数组的长度,而是将传入的整数转换大于等于initialCapacity的最小2的n次方值,例如initialCapacity=5,则真正的初始化容积为8initialCapacity=10,则真正的初始化容积为16 
}
哈希方法总结

HashMap基于Hash算法实现的

  • 当往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  • hashmap存储时,如果出现hash值相同的key,此时有两种情况。
    • 如果key相同,则覆盖原始值
    • 如果key不同出现冲突,则将当前的key-value 放入链表中[尾插法]
  • 按照key值获取数据时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值

HashMap解决hash冲突的问题核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过8个后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

简单总结

当put的时候,首先计算key的hash值,这里调用了hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以hash函数大概的作用是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。

按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length-1)&hash,如果不做hash处理,相当于散列生效的只有几个低bit位,为了减少散列的碰撞,设计综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8+中用了复杂度O(logn)红黑树结构来提升碰撞下的性能。

为什么HashMap中String、Integer这样的包装类适合作为Key

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  2. 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况;
要让自己的Object作为Key应该怎么办呢

重写hashCode()和equals()方法

  • 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞
  • 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)
    必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性
HashMap的扩容操作是怎么实现的

第一种:使用默认构造方法初始化HashMap。从前文可以知道HashMap在一开始初始化的时候会返回一个空的table,并且thershold为0。

因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12

第二种:指定初始容量的构造方法初始化HashMap。那么从下面源码可以看到初始容量会等于threshold,接着threshold = 当前的容量hreshold) * DEFAULT_LOAD_FACTOR

第三种:HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。

HashMap是怎么解决哈希冲突的

在Java中,保存数据有两种比较简单的数据结构:数组和链表。

数组的特点是:寻址容易,插入和删除困难;

链表的特点是:寻址困难,但插入和删除容易;所以将数组和链表结合在一起,发挥两者各自的优势,
使用一种叫做链地址法的方式可以解决哈希冲突

这样就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于
hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4
(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对
应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,
所以我们还需要对hashCode作一定的优化

主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任
何作用的,所以思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数
据分布更平均,我们把这样的操作称为扰动

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}

这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)

JDK1.8新增红黑树

通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的
HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn)

总结

简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

  1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
  2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
  3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快
HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标

hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~(2 ^ 30),HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置

HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行
进行异或运算,降低哈希碰撞概率也使得数据分布更平均

在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

为什么数组长度要保证为2的幂次方呢

只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;如果length为2的次幂则length-1转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快,而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为 14,对应的二进制为1110,在于h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费

为什么是两次扰动

两次扰动就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的

HashMap总结
  • 结构特点:Java8-中的HashMap是基于“数组+链表”的方式(链表法解决冲突),到了Java8,应该是“数组+链表/红黑树”的方式,默认阈值为树化8退化6
  • 线程安全:HashMap是不安全,JDK1.8-并发由于插入采用的是头插法可能会导致环形链,JDK1.8+插入数据采用的是尾插法可能会导致数据丢失。集合框架中有两种线程安全的实现Collections.synchronizedMap(new java.util.HashMap<>())和juc包中的ConcurrentHashMap,前者是锁整个表,后者采用乐观锁的方式
  • 性能特点:HashMap可以在常数时间内增加,删除,查找元素,但这也是一种平均情况,使用load factor装载因子计算阈值就是为了减少冲突带来的性能退化
  • 扩容方法:HashMap的桶数组一次扩展为原数组的2倍,控制扩展和移动的次数,这里需要执行rehash计算。如果容量小于64只会进行简单扩容,如果容量大于64则会进行树化改造。树化处理可以避免哈希碰撞攻击

Hashtable

线程安全的,不允许null的键或值;是线程安全的但是Hashtable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁。多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差

HashMap允许键和值为null,只是key只能有一个null,值允许null多个

由于线程安全,则在非多线程环境下不建议使用

Hashtable是一个散列表,它存储的内容是键值对(key-value)映射。通过"拉链法"实现的哈希表

Hashtable继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。Hashtable的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的;在hashmap中允许key和value为null,只是key只能有一个null,如果出现冲突后盖前;如果使用null的key和value则会出现一个运行时异常NullPonterException

HashMap 与 HashTable区别

  1. 线程安全:HashMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过synchronized修饰。(如果要保证线程安全的话就使用ConcurrentHashMap)
  2. 效率:因为线程安全的问题,HashMap要比HashTable效率高一点。另外HashTable基本被淘汰,不要在代码中使用它
  3. 对Null key和Null value的支持:HashMap中null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null。但是在HashTable中put进的键值只要有一个null,直接抛NullPointerException
  4. 初始容量大小和每次扩充容量大小的不同 :创建时如果不指定容量初始值,Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小。也就是说HashMap总是使用2的幂作为哈希表的大小。
  5. 底层数据结构:JDK1.8以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认8)时,将链表转化为红黑树,以减少搜索时间。Hashtable没有这样的机制。
  6. 推荐使用:在Hashtable的类注释可以看到,Hashtable是保留类不建议使用,推荐在单线程环境下使用HashMap替代,如果需要多线程使用则用ConcurrentHashMap替代。

HashMap、Hashtable和TreeMap

1、元素特性:Hashtable中key和value都不能为null,HashMap中key和value都可以使用null,TreeMap中当未实现Comparator接口时,key不可以未null;当实现Comparable接口时若未对null情况进行判断,则key不可以未null

2、顺序特性:Hashtable和HashMap具有无序特性,TreeMap时利用红黑树来实现的,默认升序排序

3、初始化与增长方式:Hashtable默认容量为11,且不要求底层数组的容量一定要为2的整数次幂;HashMap默认容量16,要求容量一定是2的整数次幂。扩容时Hashtable将容量变为原来的2倍加1,HashMap扩容将容量变为2倍

4、线程安全:hashtable是同步的,在多线程环境下效率表现非常低下,新版本中不推荐使用。hashmap不支持多线程同步,多线程会出现扩容时rehash时死循环、脏读丢失数据和size不精确的问题。如果需要同步可以使用Collections.synchronizedMap方法或者ConcurrentHashMap类。ConcurrentHashMap不仅能保证了多线程运行环境下的数据访问安全性,而且性能上也有提升

二叉树

二叉搜索树(Binary Search Tree,简称 BST),BST是一种很常用的的二叉树。它的定义是:一个二叉树中,任意节点的值要大于等于左子树所有节点的值,且要小于等于右边子树的所有节点的值。引入的原因:主要针对链表的插入和删除很快,但是是查找数据却很慢的特点。树状结构最大的优势在于查找,但是插入和删除的效率都不太高

二叉树的特点:
  • 左子树上所有结点的值均小于或等于它的根结点的值
  • 右子树上所有结点的值均大于或等于它的根结点的值
  • 左、右子树也分别为二叉排序树
    • 中序遍历:左子树——》根节点——》右子树
    • 前序遍历:根节点——》左子树——》右子树
    • 后序遍历:左子树——》右子树——》根节点
平衡树

二叉树在极端情况下会退化为链表结构,为了避免出现这个问题,引入平衡树

AVL树是一种平衡二叉树,平衡二叉树递归定义如下:

  • 左右子树的高度差小于等于1。
  • 其每一个子树均为平衡二叉树。

AVL树引入了所谓监督机制,就是在树的某一部分的不平衡度超过一个阈值后
触发相应的平衡操作。保证树的平衡度在可以接受的范围内。

满二叉树

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,深度为k(k>=1)且有(2^k)-1个结点的二叉树就是满二叉树

红黑树【面试】
  • 红黑树是一种近似平衡的二叉查找树,查找、删除、插入都快,树经常需要进行旋转达到平衡,但是平衡算法很复杂。

  • 红黑树特征:

    1、每个节点不是红色就是黑色的;

    2、根节点总是黑色的;

    3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定),(也就是从每个叶子到根的
    所有路径上不能有两个连续的红色节点);

    4、从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值