手撕HashMap源码1

目录

简单说一下什么是哈希表

哈希冲突的常见解决办法

1.开放寻址法

        1.1线性的地址检测

        1.2二次检测

        1.3双重散列

2.链表法

 HashMap的源码讲解

常见属性

初始化对象的几个构造函数

实际的扩容分析

 几个常见问题

 手写一个简单的哈希表


简单说一下什么是哈希表

其实就是在数组上面,然后不按照固定的索引去存放数据,这也是利用数组支持按照索引下标进行随机性访问的特性。其中,索引的随机生成并且保证它的唯一性,就是我们哈希表的重要任务

那么这个哈希值怎么算出来呢,一般我们会通过一个散列函数去进行计算得到,比如在HashMap的源码里面有下面这个散列函数

上面这个哈希函数可以自行设计,它的设计思想就是先拿到key的原始哈希值,这个是系统给我们实现的函数函数,在Java中,每个类都继承自Object类,而Object类中有一个默认的hashCode()方法。这默认的hashCode()方法是根据对象的内存地址计算的,因此相同内容的对象在内存中不同的位置,其hashCode()值也会不同。

总之,它能先初步计算出一个哈希值,在把这个hashCode的高16位和低16位进行混合,使得高位和低位的信息更好地交叉,进一步增加混淆性。减少了哈希冲突,哈希冲突也就是不同的对象进来你生成的哈希值一样。

那这里提一点,如果是相同的对象,你肯定要生成相同的哈希值,哈希函数也不能太复杂,不然也会耗费大量的计算时间,尽可能让算出来的哈希值随机且均匀的分布,这样也能减少冲突

一些常见的设计就比如:处余法,平方取中间数字,稍微了解一下

哈希冲突的常见解决办法

一般来说,分为两类方法来解决散列冲突:开放寻址法,链表法

1.开放寻址法

        1.1线性的地址检测

        核心:一旦出现了散列冲突,我们就重新去寻址一个空的散列地址,比如从当前位置,依         次往后面找,看看是否有空闲,直到找到空闲位置为止

        1.2二次检测

        和上面差不多的思想,只不过上面是一个地址一个地址移动往下面检测,这里把检测不上变成了原来的二次方,也就是一次可以跨很多位置进行检测

        1.3双重散列

        先用一个哈希函数计算出一个存储位置,如果位置白占用,在用第二个散列函数,直到找到空闲位置为止

2.链表法

        我们首先把数组下标的每一个位置我们可以称之为“桶(bucket)”,每一个桶通常会对应一条链表,所有的散列值相同的元素我们放到这个桶对应的链表里面

 HashMap就是采用这样的方式存储数据的

 HashMap的源码讲解

整个HashMap采用了哈希表 + 单链表 + 红黑树结构来进行设计 

常见属性

 /*** The default initial capacity - MUST be a power of two.* 默认的初始化容量*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** The maximum capacity, used if a higher value is implicitly specified* by either of the constructors with arguments.* MUST be a power of two <= 1<<30.* 最大的hash表的容量*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** The load factor used when none specified in constructor.* 加载因子,可以这样理解* 当表里面的数据达到了75%的时候,就要触发扩容*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** The bin count threshold for using a tree rather than list for a* bin.  Bins are converted to trees when adding an element to a* bin with at least this many nodes. The value must be greater* than 2 and should be at least 8 to mesh with assumptions in* tree removal about conversion back to plain bins upon* shrinkage.* 进行树化的阈值,默认某个桶结点数大于8进行树化* 但是还是有个条件是,整个桶的所有结点满足最小的树化容量(MIN_TREEIFY_CAPACITY = 64)*/static final int TREEIFY_THRESHOLD = 8;/*** The bin count threshold for untreeifying a (split) bin during a* resize operation. Should be less than TREEIFY_THRESHOLD, and at* most 6 to mesh with shrinkage detection under removal.* 链化的阈值,当结点数小于6时,红黑树会退化成单链表*/static final int UNTREEIFY_THRESHOLD = 6;/*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.* 树化一个条件:表的最小容量数目*/static final int MIN_TREEIFY_CAPACITY = 64;/*** Basic hash bin node, used for most entries.  (See below for* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)* 每一个结点*/static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + "=" + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}

这里先来说jdk1.8的源码

初始化对象的几个构造函数

/*** Constructs an empty <tt>HashMap</tt> with the specified initial* capacity and load factor.** @param  initialCapacity the initial capacity* @param  loadFactor      the load factor* @throws IllegalArgumentException if the initial capacity is negative*         or the load factor is nonpositive*/public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);//上面几个都是一些常规的安全检查this.loadFactor = loadFactor;//赋值你的负载因子//这个函数返回的阈值是一个最接近当前初始容量的二次方数this.threshold = tableSizeFor(initialCapacity);//返回一个阈值//需要注意一点,调用这个构造方法的时候//还没有给我们返回容量,也就是这个表还没有被初始化}/*** Constructs an empty <tt>HashMap</tt> with the specified initial* capacity and the default load factor (0.75).** @param  initialCapacity the initial capacity.* @throws IllegalArgumentException if the initial capacity is negative.* 还是调用上面那个构造函数*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}/*** Constructs an empty <tt>HashMap</tt> with the default initial capacity* (16) and the default load factor (0.75).* 给了一个默认的负载因子*/public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}/*** Constructs a new <tt>HashMap</tt> with the same mappings as the* specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with* default load factor (0.75) and an initial capacity sufficient to* hold the mappings in the specified <tt>Map</tt>.** @param   m the map whose mappings are to be placed in this map* @throws  NullPointerException if the specified map is null* 一样给一个默认的负载因子* 但是这里会调用一个putMapEntries方法进行数据的追加*/public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}/*** Implements Map.putAll and Map constructor.** @param m the map* @param evict false when initially constructing this map, else* true (relayed to method afterNodeInsertion).* 当使用HashMap(map)初始化一个对象的时候,会进入这个构造方法* 这个方法会计算出一个阈值,这个阈值一般和你传入的map大小是有关系的* 然后在*/final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {//表为null的时候,计算一个扩容阈值if (table == null) { // pre-sizefloat ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);if (t > threshold) //原始阈值肯定为0,进来threshold = tableSizeFor(t);//变成当前值最近的二次方,这个数组大小和s大小有关系}else if (s > threshold)resize();//表如果不为空,容量大于了阈值,就进行扩容//循环添加数据for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();//内部还是调用了putVal开始添加数据putVal(hash(key), key, value, false, evict);}//上面第一次进来,也会先算阈值,然后进行循环调用putVal进行数据的添加}}

这里就得引入一个问题,就是哈希表在什么时候进行初始化,上面看了一下构造函数,在初始化的时候new HashMap()与new HashMap(capacity,factor)他们其实没有创建表,只是给我们创建了一个负载因子和阈值(这个阈值是在后面创建的),他们初始化哈希表是在调用put方法添加数据的时候进行的,那我们在看new HashMap(Map)这个构造函数,它虽然也会给我们先算一个阈值出来,但是后面会调用一个for循环调用putVal方法进行数据添加,这个时候就会初始化一张表

如果说非要问,哈希表是在什么时候进行初始化的?

我们可以这样回答,空参构造和带容量的构造函数初始化一个对象的时候,没有初始化表,是在调用put方法的时候进行初始化的,而我们利用map集合进行初始化一个对象的时候,它是会给我们初始化表的,但是本质也是调用了putVal方法

构造对象看完了,下面就来看put和putVal这两个初始化一张表的方法

其实put方法内部就是调用了putVal进行一个数据的添加,下面我们看一下表的初始化过程

这里面有putVal方法的详细注释

下面我引入了完整的put方法

我们可以这样来看

第一次put数据会先进入下面这部分代码里面

然后去调用扩容方法reszie,那么进入到resize之后,会直接干到if (oldTab != null)的部分,我把这部分代码贴过来

/****扩容的函数* @return the table* 先来分析一下他会进入扩容的一些情况* 1.第一次put的时候,会调用putVal方法,然后内部,会调用resize()方法进行初始化一个哈希表*阈值对于HashMap(initialCapacity, loadFactor)来说会直接根据capacity算出一个阈值,new一个构造对象的时候就会算出来*对于HashMap()来说,是在第一次初始化算阈值,也就是在第一次put的时候计算阈值***/final Node<K,V>[] resize() {//oldTab:引用扩容前的哈希表//这个表对于所有构造函数第一次put进来的时候来讲是nullNode<K,V>[] oldTab = table;//为null长度自然就为0,否则返回之前表的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;//拿到当前对象的阈值int oldThr = threshold;//新长度,新的阈值int newCap, newThr = 0;//这一块计算新的长度与阈值,分为不同的块来看//原来的表有长度,那么肯定不是new HashMap(),因为它初始化的时候,不可能有长度//那么也不是new HashMap(cap,factor)这个,这个只会根据cap计算一个2次方的阈值,也没有长度//考虑为第一次为初始化map的时候,new HashMap(map),这个也不会有长度,但是会根据map的大小//计算出来一个阈值,这个阈值也是2的次方数目,然后就在添加数据进行表的初始化,也没有长度if (oldCap > 0) {//第一把初始化时构造函数的时候,都不可能走到先走到这,但是对于new HashMap(map)它是在构造函数内部调用putVal然后走到这的if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}//这里就是oldCap等于0的情况,但是它又是一个有阈值的情况//什么情况在初始化的时候会有阈值呢?那么肯定就是new HashMap(cap,factor), new HashMap(map)都有//那么他们就会走这初始化,把原来的阈值变为新的长度,但是newThr在这还没有初始化else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//下面就是oldCap等于0,oldThr=0//就是空参new HashMap(),初始化的时候无长度,无阈值else {               // zero initial threshold signifies using defaults//问题:HashMap无参构造调用的时候,什么时候会初始化一张哈希表//在第一次put的时候,会初始化一表,他会走oldCapacity=0与oldThreshold=0的情况//把长度变为默认长度16,把阈值变为默认负载因子*默认容量=0.75*16=12newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//新的阈值为0,经过上面,只有new HashMap(cap,factor)与new HashMap(map)新的阈值为0if (newThr == 0) {//这里对于HashMap(map),它的负载因子也是0.75float ft = (float)newCap * loadFactor;//算新的阈值 = 老的阈值(因为之前是老的阈值给到了新的长度)*他们传入的负载因子newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}//然后把当前对象的阈值赋值上去,什么new初始化都是在这里赋值新的阈值threshold = newThr;//上面做完了,该初始化的都初始化完了@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//建立一个新的哈希表,如果是初始化,就是第一次在这里建立一张表table = newTab;//然后赋值给table;//以上初始化到这一步就全部结束掉,因为老的表是没有数据的//对所有的new HashMap(....)都是一样的结果,也就是在put里面第一次调用resize()走到这就结束了if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {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 orderNode<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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;}

 下面是完整的puVal方法注释

/*** Associates the specified value with the specified key in this map.* If the map previously contained a mapping for the key, the old* value is replaced.** @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with <tt>key</tt>, or*         <tt>null</tt> if there was no mapping for <tt>key</tt>.*         (A <tt>null</tt> return can also indicate that the map*         previously associated <tt>null</tt> with <tt>key</tt>.)*/public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}/*** Implements Map.put and related methods.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//tab:引用当前hashMap的散列表//p: 当前散列表的元素//n: 散列表数组的长度//i: 根据上面的hash进一步计算出的数据存放在哈希表的位置Node<K,V>[] tab; Node<K,V> p; int n, i;//不管用什么构造函数进行初始化一个HashMap//他们都没有初始化一张hash表//所以都会进入到这个里面,进行扩容,并且把长度赋值给nif ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//按照分析//所有的HashMap构造函数都会在上面一步进行一个扩容//与其说是扩容,不如说是初始化一张哈希表//然后在进入下面的判断//上面哈希表就已经被初始化出来了//i = (n - 1) & hash 利用之前hash码算一个哈希位置出来//并把当前元素赋值给p//如果当前值为NULLif ((p = tab[i = (n - 1) & hash]) == null)//这个里面的n在每一次扩容之前都不会变,换句话,如果每次的key一样,并且没有主动去修改此对象的hashCode,大多返回的i都一样//直接把值放进去tab[i] = newNode(hash, key, value, null);//如果这个位置已经有数据了//注意一个问题是位置有数据,并不代表key就相等//哈希码肯定相等的//但是你看它计算位置的方式(n - 1) & hash 其中n是在变化的,n是数组大小//所以算出i的索引值也是在发生变化的,可能相等可能不相等//这个else大体的判定条件是!= nullelse {//e: 找到一个与插入key相等的结点,这里也就是key相等的结点//k: 一个临时的kNode<K,V> e; K k;//上面if判断的时候就已经把当前元素赋值给了p//当前元素hash码相等 && key相等//其实这里完全可以理解为key相等,计算的hash码就是相等的,自然i索引位置相等//这里也就是在第一个位置,也就是表的桶位,进行了判定if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//key相等的索引结点赋值给e,后面需要去把e的value替换掉//上面没进去说明key不一样//然后我们判断下面当前结点是不是一棵红黑树//如果是红黑树,就按照红黑树的方式插入进去else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//上面没有相等的key替换,也没有被树化//那就是链表处理else {//走一个循环插入//jdk1.8是尾插,所以要走指针到链表的最后一个位置for (int binCount = 0; ; ++binCount) {//如果当前结点的.next等于null的时候,表明已经走到了链表的最后一个结点if ((e = p.next) == null) {//直接把这个结点连接到当前结点的next后面p.next = newNode(hash, key, value, null);//binCount记录了这个链表走了多少次//TREEIFY_THRESHOLD=8这是树化的条件//当binCount = 7的时候,也就是达到了树化的条件,循环了八次if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//如果在循环的过程中找到key相等的位置,就需要替换值//因为链表里面可能也存在相等的key啊//直接跳出这个循环就行if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;//这就是让p与e交替往下面轮替}}//替换e结点if (e != null) { // existing mapping for keyV oldValue = e.value;//保留老值,等会返回//onlyIfAbsent表示存在某个key就不插入这里默认是false//不为false,进入,表示已经存在某个key值if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;//表的改变次数,包含了添加操作,删除操作等改变//哈希表的长度大于了阈值,就要进行扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

实际的扩容分析

扩容会走下面这条路

如果表里面的数量大于了阈值,就开始扩容,下面是resize扩容部分的代码分析

 final Node<K,V>[] resize() {//拿到老表Node<K,V>[] oldTab = table;//老表这里,肯定会拿到长度//这里说一下如果是空参构造,在上面初始化put之后长度就是16(值默认)//如果是带容量的构造函数,在上面初始化后容量是2次方的值,至于具体是多少//这个要根据你传入的initialCapacity决定,因为他会先算一个阈值,然后在下次//初始化表的时候,把这个阈值当做容量传递过去,然后在根据你的容量和传入的负载因子//算出一个新的阈值//而对于利用map集合进行初始化一个HashMap的时候,它的长度取决于map集合//的长度,因为他是利用map集合的长度来算出了一个2次方的值作为阈值,下次初始化//一张表的时候,在把这个阈值拿进来作为表的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;//拿到当前对象的阈值,也是有的int oldThr = threshold;//新长度,新的阈值初始化为0int newCap, newThr = 0;//这里就不是初始化动作了//这里全部都是扩容,所以都走这条if路线,容量全部都是大于0的if (oldCap > 0) {//直接封顶了,也就是扩容不了,直接返回老表if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//重点在这个位置//新的表的长度一定会扩展为原来表长度的2倍//新的阈值这里分了一个情况:只有老表的长度大于了默认的容量//才会把新的阈值扩展为原来的2倍//那么对于空参构造来说,这里肯定就是新的长度与阈值全都变为原来的2倍//因为它的初始化长度就是16默认值//那么什么情况下会出现oldCap小于DEFAULT_INITIAL_CAPACITY这个容量呢//那么就是当用另外两个构造函数初始化一个哈希表的时候//你传入的capacity与map集合的大小非常小的时候//这里拿capacity来说,当它的值是8以下的时候//他会先算出来一个初始阈值,这个初始阈值在第一次初始化哈希表的时候//就会给这张表作为初始容量,假设是8以下,比如4,那么初始容量算出来就是8//阈值就是,假设负载因子传入的是0.5,阈值=8*0.5=4也就是olcCap=8,oldThr=4//下一次进来扩容长度变为16,因为老的长度是8<16,所以它的阈值扩容不会直接走//什么下面的两倍,而是跳到下面我标注(1)的位置else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) newCap = oldThr;else {               newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//(1)//如果到这个位置//那么肯定是oldCapacity长度<16if (newThr == 0) {//它按照新长度和它自己的负载因子//上面我们假设了负载因子是0.5,新长度是16 //那么乘以新的负载因子就是16*0.5= 8;//我们会注意到阈值也还是变为了原来的2倍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;//然后赋值给table;//扩容老表肯定不等于null的,所以必走下面这条路if (oldTab != null) {//遍历整个表,我们重点在于对原来表的数据的迁移for (int j = 0; j < oldCap; ++j) {Node<K,V> e;//中间的移动变量//当这个位置有值了,让e结点去指向这个位置if ((e = oldTab[j]) != null) {oldTab[j] = null;//把当前位置的数据变为null//如果当前位置下面没有结点//说明了一个问题就是当前结点是一个唯一的key//也就是说按照之前的hash算出来的位置没有重复值//那么直接算出一个位置,放到新表里面//这个位置不可能重复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 {//这里分为迁移到低位链表还是高位链表//低位链表原来的链表长度之内//高位链表扩展出来的链表长度之内Node<K,V> loHead = null, loTail = null;//低位链表Node<K,V> hiHead = null, hiTail = null;//高位链表Node<K,V> next;//中间的移动指针do {next = e.next;//如果最高位为0的数据,依旧放在原来的表的位置if ((e.hash & oldCap) == 0) {if (loTail == null)//这个只会进来一次loHead = e;//当前loHead是表头,到时候挂到桶位置上的表头elseloTail.next = e;loTail = e;//loTail会指向e的上一个结点}else {if (hiTail == null)hiHead = e;//同理elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {//这里刚好是这个链表的最后一个数据//它的next直接就是null//假如这里不是当前链表的最后一个数据,他可能下面还有数据//因为他已经要搬到新位置上去//所以直接把这里的next直接挂为nullloTail.next = null;//低位表最后一个位置next挂为nullnewTab[j] = loHead;}if (hiTail != null) {//同理hiTail.next = null;//扩展到了新的位置newTab[j + oldCap] = hiHead;}}}}}return newTab;}

简单说一下如果表的长度达到了最大值处理方式

这里还得重点来说一下关于链表位置的迁移计算

那么上面就知道了e.hash & oldCap结果如果为0的情况,那就是最高位一定为0的情况,最高位如果为1,它永远不会出现为0的情况,那么继续往下面看

 下面我们就可以拿到当前的结点的hash值我们去计算一下位置

很明显,高位为0的结点的hash值,它的位置只与后面四位有关,换句话说说,上面的hash值不管是与老的长度位比较还是新的长度位比较,结果都一样,所以他们全都放在低位链表的位置,换句话,这几个结点的位置全都不动

那么我们现在去看一下扩展到高位的链表

 这里也就说明了,为什么高位链表要扩展到这个位置的原因

 几个常见问题

 

 手写一个简单的哈希表

package com.pxx.test.hashmap.myhashmap1;public class MyHashMap<K, V> {//默认容量private static final int DEFAULT_CAPACITY = 16;//容量的最大值private static int MAXIMUM_CAPACITY = 1 << 30;//默认负载因子private static final float DEFAULT_LOAD_FACTOR = 0.75f;//哈希表的容量private int capacity;//负载因子private float loadFactor;//阈值private int threshold;//当前哈希映射中键值对的数量private int size;//存储键值对的数组,每一个元素都是一个链表头结点private Node<K, V>[] table;public MyHashMap() {//使用默认的容量和负载因子创建哈希映射实例this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);}/*** //带参,使用指定容量和负载因子创建哈希映射实例* @param capacity* @param loadFactor*/public MyHashMap(int capacity, float loadFactor) {this.capacity = capacity;this.loadFactor = loadFactor;this.table = new Node[capacity];//这里已经把数据表给初始化了//把阈值在初始化的时候也给上this.threshold = (int) (capacity * loadFactor);}/*** 内部静态节点类,表示哈希映射中的结点* 这个结点的设计除了键值以外* 应该还包含一个指向下一个node的指针,* 因为当某个索引算出来* 但是又被占用了之后,后面就要接链表了,不停的往下指向*/private static class Node<K, V> {K key;V value;Node<K, V> next;//每个结点后面可能变成一个链表,所以必须有一个next指针int hash;//给一个hash值用于后面进行扩容操作public Node(int hash , K key, V value, Node<K, V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}}//这里提供一个哈希函数,计算哈希索引的值private int hash(K key) {return key.hashCode();}//完善添加方法//这里面采用了链接的方法解决了冲突//如果有相同的键进来,就要被后面的值进行更新public V  put(K key, V value) {int index = hash(key);//拿到哈希值//拿到表的长度int n = table.length;//当前这个结点就是数组的桶,也就是链表的头结点Node<K, V> node = table[index & (n - 1)];//从这个当前哈希索引里面拿一个节点看看//如果不等于null//表明这个位置有值,我们就要插入到链表里面去//那么是头插还是尾插呢//在JDK源码里面1.8是尾插 1.8之前是头插//那我们这里实现尾插if (node != null) {while (node.next != null) {//还必须知道如果存在相同的键,那么就需要把值进行更新//但是如果更新了我们需要返回老值if (node.key.equals(key)) {V oldVal = node.value;//然后替换值node.value = value;return oldVal;}node = node.next;//最后跳出循环会移动到最后一个指针的位置}Node<K,V> newNode = new Node<>(index,key, value, null);node.next = newNode;} else {//直接放到这个位置就行了table[index] = new Node<>(index, key, value, null);}//除了替换老值,不进入size的计算之内//其他的都要进入到size的计算之内size++;//看看是否达到了负载因子//默认负载因子//private static final float DEFAULT_LOAD_FACTOR = 0.75f;//这里的负载因子默认是75%,比如容量是16 * 0.75 = 12//如果达到了12,这个哈希表的容量也就达到了一个阈值//负载因子用于控制哈希表的填充程度//当负载因子设置得较小时,哈希表会更早地进行扩容,以减少冲突和提高性能if (size > threshold) {resize();}return null;//没有新的值}//我限定的是只要进入这个方法就是扩容//表的初始化动作没有在这里进行private Node<K,V>[] resize() {Node<K, V>[] oldTab = table;//拿到老表的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;//拿到阈值int oldThr = threshold;//新长度,新阈值int newCap = 0, newThr = 0;if (oldCap > 0) {//容量如果是最大值,封顶//阈值给个最大值,返回老表if (oldCap >= MAXIMUM_CAPACITY) {//直接把阈值给到最大threshold = Integer.MAX_VALUE;return oldTab;}//这里我们直接扩展两倍newCap = oldCap << 1;newThr = oldThr << 1;}//新的阈值赋值threshold = newThr;//创建新表,注意强制类型转换,编译器无法强制类型转换成泛型数组Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];table = newTab;if (oldTab != null) {Node<K, V> e;//给一个中间结点,等会用来移动//遍历老表for (int j = 0; j < oldCap; ++j) {e = oldTab[j];//当前结点有值if (e != null) {//把老表的位置释放掉oldTab[j] = null;//下面去判断是直接放值,还是进行链化迁移//没有一个重复的索引位置if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else {//考虑为链表的迁移Node<K, V> loHead = null, loTail = null;Node<K, V> hiHead = null, hiTail = null;Node<K, V> next;do {next = e.next;//最高位如果为0,放原来的位置if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;//用loTail不断去拼接这个链表loTail = e;//loTail不停往下面走,然后会卡在其中某一个位置} else {//最高位不等于0的时候,考虑为1的时候//会迁移到原来的位置 + 最高位数if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}//然后整体把e向下移动e = next;} while (e != null);//直到结束大家都链好了各自的数据//上面循环完了之后,要把链表挂到相应的位置上去//这里就是真正的迁移if (loTail != null) {//这里刚好是这个链表的最后一个数据//它的next直接就是null//假如这里不是当前链表的最后一个数据,他可能下面还有数据//因为他已经要搬到新位置上去//所以直接把这里的next直接挂为nullloTail.next = null;//低位表最后一个位置next挂为nullnewTab[j] = loHead;}if (hiTail != null) {//同理hiTail.next = null;//扩展到了新的位置newTab[j + oldCap] = hiHead;}}}}}return newTab;}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/257489.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

接口测试要测试什么?

第一部分&#xff1a; 首先&#xff0c;在做接口测试的过程中&#xff0c;经常有后端开发会问&#xff1a; 后端接口都测试什么&#xff1f;怎么测的&#xff1f;后端接口测试一遍 &#xff0c;前端也测试一遍&#xff0c;是不是重复测试了&#xff1f; 于是&#xff0c;为了…

爱智EdgerOS之深入解析AI图像引擎如何实现AI视觉开发

一、前言 AI 视觉是为了让计算机利用摄像机来替代人眼对目标进行识别&#xff0c;跟踪并进一步完成一些更加复杂的图像处理。这一领域的学术研究已经存在了很长时间&#xff0c;但直到 20 世纪 70 年代后期&#xff0c;当计算机的性能提高到足以处理图片这样大规模的数据时&am…

实验3.5 路由器的单臂路由配置

实验3.5 路由器的单臂路由配置 一、任务描述二、任务分析三、具体要求四、实验拓扑五、任务实施1.SWA的基本配置2.RA的基本配置3.在RA上查看接口状态 六、任务验收七、任务小结 一、任务描述 某公司对部门划分了需VLAN之后&#xff0c;发现两个部门之间无法通信&#xff0c;但…

Kubernetes集群安装高可用postgresql

Kubernetes集群安装高可用postgresql Bitnami 提供的 postgresql-ha 解决方案是一个预配置的、高可用的 PostgreSQL 集群配置&#xff0c;通常部署在 Kubernetes 环境中。它使用了一些关键技术和组件来实现数据库的高可用性。&#xff0c;Bitnami postgresql-ha 主要采用以下构…

【PCB设计】嘉立创EDA器件3D模型导入AD的方法

嘉立创EDA器件3D模型导入AD的方法 一、嘉立创EDA导出3D模型二、CAD编辑3D模型三、AD中加载3D模型 一、嘉立创EDA导出3D模型 在嘉立创EDA中找到对应的元器件&#xff0c;并生成PCB&#xff0c;选择导出3D文件 导出元件step模型 二、CAD编辑3D模型 用FreeCAD打开模型 删除…

pytorch的二次索引矩阵无法赋值问题

最近在研究中发现torch一个问题&#xff0c;即torch的二次索引的矩阵无法赋值。 具体来说&#xff0c;给定相同的初始常数矩阵a和iou_target矩阵, 以及另一iou矩阵&#xff0c;直接赋值是没问题的。 然而&#xff0c;当对iou_target矩阵进行二次索引时&#xff0c;即idx矩阵和…

【小白专用】MySQL创建数据库和创建数据表

1.在Windows开始搜索输入Mysql,并选择第一个打开。 2.输入安装时的密码 3.说明安装成功。 二、创建数据库 1. 连接 MySQL 输入 mysql -u root -p 命令&#xff0c;回车&#xff0c;然后输入 MySQL 的密码(不要忘记了密码)&#xff0c;再回车&#xff0c;就连接上 MySQL 了。 …

leetcode做题笔记1466. 重新规划路线

n 座城市&#xff0c;从 0 到 n-1 编号&#xff0c;其间共有 n-1 条路线。因此&#xff0c;要想在两座不同城市之间旅行只有唯一一条路线可供选择&#xff08;路线网形成一颗树&#xff09;。去年&#xff0c;交通运输部决定重新规划路线&#xff0c;以改变交通拥堵的状况。 路…

全光谱台灯对孩子眼睛好吗?备考护眼台灯推荐

全光谱台灯通常被认为对孩子的眼睛更好&#xff0c;因为它们能够提供更接近自然光的光谱。与传统的白炽灯或荧光灯相比&#xff0c;全光谱台灯能够提供更均匀、真实的光线&#xff0c;减少眼睛的疲劳和视觉疲劳。此外&#xff0c;全光谱台灯还可以提供更好的颜色还原&#xff0…

vue 实现返回顶部功能-指定盒子滚动区域

vue 实现返回顶部功能-指定盒子滚动区域 html代码css代码返回顶部显示/隐藏返回标志 html代码 <a-icontype"vertical-align-top"class"top"name"back-top"click"backTop"v-if"btnFlag"/>css代码 .top {height: 35px;…

PLC无线通讯终端在二氧化碳注气开采石油中的应用

一、应用背景 在传统的石油开采过程中&#xff0c;只能采收到地下原油储层中约30%至40%的石油。二氧化碳强化石油开采技术是一种利用二氧化碳来提高石油采收率的技术。将工业尾气中的二氧化碳被捕集起来&#xff0c;注入油田地下油层&#xff0c;把原油"驱赶”出来&#…

前端性能优化的一些方法和策略

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…