HashMap的实现原理及hash冲突解决方法

面试复盘:哈希冲突的常见解决方案? Java 面试中不可能不问 HashMap,问到 HashMap 就会问到哈希冲突的解决方案,相信很多人也遇到过了,所以这里就详细的总结复盘一下。哈希冲突是指在哈希表中,两个或多个元素被映射到了同一个位置的情况。程序的运行结果如下:不同的字符串,却拥有了相同的 hashCode 这就是哈希冲突。因为元素的位置是根据 hashCode 的值进行定位的,此时它们的 hashCode 相同,但一个位置只能存储一个值,这就是哈希冲突。 阅读详情

HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。源码如下:

  1.    public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.             Object k;  
  8.             //判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。  
  9.             //如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。  
  10.             //Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。  
  11.             //系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),  
  12.             //那系统必须循环到最后才能找到该元素。  
  13.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  14.                 V oldValue = e.value;  
  15.                 e.value = value;  
  16.                 return oldValue;  
  17.             }  
  18.         }  
  19.         modCount++;  
  20.         addEntry(hash, key, value, i);  
  21.         return null;  
  22.     }  
  23.    

       上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可.HashMap程序经过我改造,我故意的构造出了hash冲突现象,因为HashMap的初始大小16,但是我在hashmap里面放了超过16个元素,并且我屏蔽了它的resize()方法。不让它去扩容。这时HashMap的底层数组Entry[]   table结构如下: 

   

       Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表。形成单链表的核心代码如下:

 

  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     Entry<K,V> e = table[bucketIndex];  
  3.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.     if (size++ >= threshold)  
  5.         resize(2 * table.length);  
  6. bsp;  

     上面方法的代码很简单,但其中包含了一个设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

       HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

       当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。

一、HashMap概述

  HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

  值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。

 Map map = Collections.synchronizedMap(new HashMap());

 

二、HashMap的数据结构

  HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同学都知道,解决hash冲突的方法有很多(可参考hashMap冲突处理http://www.cnblogs.com/hapjin/p/4858505.html?ptvd),HashMap底层是通过链表来解决hash冲突的。

 

 图中,紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。

我们看看HashMap中Entry类的代码:

 

复制代码
复制代码
    /** Entry是单向链表。    
     * 它是 “HashMap链式存储法”对应的链表。    
     *它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数  
    **/  
    static class Entry<K,V> implements Map.Entry<K,V> {    
        final K key;    
        V value;    
        // 指向下一个节点    
        Entry<K,V> next;    
        final int hash;    
   
        // 构造函数。    
        // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"    
        Entry(int h, K k, V v, Entry<K,V> n) {    
            value = v;    
            next = n;    
            key = k;    
            hash = h;    
        }    
   
        public final K getKey() {    
            return key;    
        }    
   
        public final V getValue() {    
            return value;    
        }    
   
        public final V setValue(V newValue) {    
            V oldValue = value;    
            value = newValue;    
            return oldValue;    
        }    
   
        // 判断两个Entry是否相等    
        // 若两个Entry的“key”和“value”都相等,则返回true。    
        // 否则,返回false    
        public final boolean equals(Object o) {    
            if (!(o instanceof Map.Entry))    
                return false;    
            Map.Entry e = (Map.Entry)o;    
            Object k1 = getKey();    
            Object k2 = e.getKey();    
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {    
                Object v1 = getValue();    
                Object v2 = e.getValue();    
                if (v1 == v2 || (v1 != null && v1.equals(v2)))    
                    return true;    
            }    
            return false;    
        }    
   
        // 实现hashCode()    
        public final int hashCode() {    
            return (key==null   ? 0 : key.hashCode()) ^    
                   (value==null ? 0 : value.hashCode());    
        }    
   
        public final String toString() {    
            return getKey() + "=" + getValue();    
        }    
   
        // 当向HashMap中添加元素时,绘调用recordAccess()。    
        // 这里不做任何处理    
        void recordAccess(HashMap<K,V> m) {    
        }    
   
        // 当从HashMap中删除元素时,绘调用recordRemoval()。    
        // 这里不做任何处理    
        void recordRemoval(HashMap<K,V> m) {    
        }    
    }
复制代码
复制代码

 

HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。

 

三、HashMap源码分析

 

       1、关键属性

  先看看HashMap类中的一些关键属性:

 

复制代码
复制代码
1 transient Entry[] table;//存储元素的实体数组
2  
3 transient int size;//存放元素的个数
4  
5 int threshold; //临界值   当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
6 
7  final float loadFactor; //加载因子
8  
9 transient int modCount;//被修改的次数
复制代码
复制代码

 

其中loadFactor加载因子是表示Hsah表中元素的填满的程度.

若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。

反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)

冲突的机会越大,则查找的成本越高.

因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.

  如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。

 

2、构造方法

下面看看HashMap的几个构造方法:

 

复制代码
复制代码
public HashMap(int initialCapacity, float loadFactor) {
 2         //确保数字合法
 3         if (initialCapacity < 0)
 4             throw new IllegalArgumentException("Illegal initial capacity: " +
 5                                               initialCapacity);
 6         if (initialCapacity > MAXIMUM_CAPACITY)
 7             initialCapacity = MAXIMUM_CAPACITY;
 8         if (loadFactor <= 0 || Float.isNaN(loadFactor))
 9             throw new IllegalArgumentException("Illegal load factor: " +
10                                               loadFactor);
11 
12         // Find a power of 2 >= initialCapacity
13         int capacity = 1;   //初始容量
14         while (capacity < initialCapacity)   //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
15             capacity <<= 1;
16 
17         this.loadFactor = loadFactor;
18         threshold = (int)(capacity * loadFactor);
19         table = new Entry[capacity];
20        init();
21    }
22 
23     public HashMap(int initialCapacity) {
24         this(initialCapacity, DEFAULT_LOAD_FACTOR);
25    }
26 
27     public HashMap() {
28         this.loadFactor = DEFAULT_LOAD_FACTOR;
29         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
30         table = new Entry[DEFAULT_INITIAL_CAPACITY];
31        init();
32     }
复制代码
复制代码

 

我们可以看到在构造HashMap的时候如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则的话就是用默认的。默认初始容量为16,默认加载因子为0.75。我们可以看到上面代码中13-15行,这段代码的作用是确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂,至于为什么要把容量设置为2的n次幂,我们等下再看。

 

重点分析下HashMap中用的最多的两个方法put和get

       3、存储数据

  下面看看HashMap存储数据的过程是怎样的,首先看看HashMap的put方法:

  

复制代码
复制代码
public V put(K key, V value) {
     // 若“key为null”,则将该键值对添加到table[0]中。
         if (key == null) 
            return putForNullKey(value);
     // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
         int hash = hash(key.hashCode());
     //搜索指定hash值在对应table中的索引
         int i = indexFor(hash, table.length);
     // 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
         for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
             Object k;
              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值
                  V oldValue = e.value;
                 e.value = value;
                 e.recordAccess(this);
                 return oldValue;
              }
         }
     //修改次数+1
         modCount++;
     //将key-value添加到table[i]处
     addEntry(hash, key, value, i);
     return null;
}
复制代码
复制代码

 

上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

我们慢慢的来分析这个函数,第2和3行的作用就是处理key值为null的情况,我们看看putForNullKey(value)方法:

 

复制代码
复制代码
1 private V putForNullKey(V value) {
 2         for (Entry<K,V> e = table[0]; e != null; e = e.next) {
 3             if (e.key == null) {   //如果有key为null的对象存在,则覆盖掉
 4                 V oldValue = e.value;
 5                 e.value = value;
 6                 e.recordAccess(this);
 7                 return oldValue;
 8            }
 9        }
10         modCount++;
11         addEntry(0, null, value, 0); //如果键为null的话,则hash值为0
12         return null;
13     }
复制代码
复制代码

 

注意:如果key为null的话,hash值为0,对象存储在数组中索引为0的位置。即table[0]

我们再回去看看put方法中第4行,它是通过key的hashCode值计算hash码,下面是计算hash码的函数:

 

复制代码
复制代码
1  //计算hash值的方法 通过键的hashCode来计算
2     static int hash(int h) {
3         // This function ensures that hashCodes that differ only by
4         // constant multiples at each bit position have a bounded
5         // number of collisions (approximately 8 at default load factor).
6         h ^= (h >>> 20) ^ (h >>> 12);
7         return h ^ (h >>> 7) ^ (h >>> 4);
8     }
复制代码
复制代码

 

得到hash码之后就会通过hash码去计算出应该存储在数组中的索引,计算索引的函数如下:

 

1     static int indexFor(int h, int length) { //根据hash值和数组长度算出索引值
2         return h & (length-1);  //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出
3     }

 

这个我们要重点说下,我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。

 

    接下来,我们分析下为什么哈希表的容量一定要是2的整数次幂。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

 

  这看上去很简单,其实比较有玄机的,我们举个例子来说明:

  假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下: 

复制代码
       h & (table.length-1)                     hash                             table.length-1
       8 & (15-1):                             1000                   &              1110        =       1000
       9 & (15-1):                             1001                   &              1110        =       1000
       --------------------------------------------------------------------------------------------------------
       8 & (16-1):                             1000                   &              1111        =       1000
       9 & (16-1):                             1001                   &              1111        =       1001
复制代码

 

从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

   所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

   

       根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

 

 

复制代码
1 void addEntry(int hash, K key, V value, int bucketIndex) {
2         Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点
3         table[bucketIndex] = new Entry<>(hash, key, value, e);
4         if (size++ >= threshold) //如果大于临界值就扩容
5             resize(2 * table.length); //以2的倍数扩容
6     }
复制代码

 

参数bucketIndex就是indexFor函数计算出来的索引值,第2行代码是取得数组中索引为bucketIndex的Entry对象,第3行就是用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。

  第4行和第5行就是判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容,HashMap扩容是扩为原来的两倍。

 

4、调整大小

resize()方法如下:

 重新调整HashMap的大小,newCapacity是调整后的单位

复制代码
复制代码
 1     void resize(int newCapacity) {
 2         Entry[] oldTable = table;
 3         int oldCapacity = oldTable.length;
 4         if (oldCapacity == MAXIMUM_CAPACITY) {
 5             threshold = Integer.MAX_VALUE;
 6             return;
 7        }
 8 
 9         Entry[] newTable = new Entry[newCapacity];
10         transfer(newTable);//用来将原先table的元素全部移到newTable里面
11         table = newTable;  //再将newTable赋值给table
12         threshold = (int)(newCapacity * loadFactor);//重新计算临界值
13     }
复制代码
复制代码

 

新建了一个HashMap的底层数组,上面代码中第10行为调用transfer方法,将HashMap的全部元素添加到新的HashMap中,并重新计算元素在新的数组中的索引位置

 

 

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

 

   那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

 

 

 5、数据读取

 

 

复制代码
复制代码
1.public V get(Object key) {   
2.    if (key == null)   
3.        return getForNullKey();   
4.    int hash = hash(key.hashCode());   
5.    for (Entry<K,V> e = table[indexFor(hash, table.length)];   
6.        e != null;   
7.        e = e.next) {   
8.        Object k;   
9.        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))   
10.            return e.value;   
11.    }   
12.    return null;   
13.}  
复制代码
复制代码

有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

 

6、HashMap的性能参数:

 

   HashMap 包含如下几个构造器:

   HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。

   HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。

   HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

   HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。

   initialCapacity:HashMap的最大容量,即为底层数组的长度。

   loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。

   负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。

   HashMap的实现中,通过threshold字段来判断HashMap的最大容量:

 

threshold = (int)(capacity * loadFactor);  
[java]  view plain  copy
  1.   

   结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍。



---------------------------------------------------------------------------------------------------------------------------------------------------

原文地址:http://zhangshixi.iteye.com/blog/672697

PID详解 1、什么是PID及其作用 设定一个输出目标,反馈系统传回输出值,如与目标不一致,则存在一个误差,PID 根据此误差调整输入值,直至输出达到设定值. 为什么需要PID呢,比如控制温度,我们需要设置一个要达到的目标温度值,当将温度稳定在目标温度值 例如,我们希望水的目标温度值为70°,其希望无非就是可以快速而且无抖动的达到70°,并稳定在70° 如果我们使用普通的方式加热,当温度到达70°后就以及停止加热,岂不是简单快速,当然用在要求不高的场合也可以,但是如果要求更高的场合呢,会出现温度到达70°后...... 阅读详情

相关推荐

三极管稳压电路仿真分析

Z1的负极连接到三极管的基极,三极管发射极E是输出,形成了一个射极跟随器,也就是说,E极的电压随着B电压变化而变化,但BE之间有一个压降,一般硅管0.7V左右,假设B极是6.7V,那E极就是6.7-0.7=6V,所以就有一个公式VOUT=Vz-Vbe,可以根据VOUT来选择合适的稳压管。从稳压管的SPEC中我们可以看到,Vz@Izt这个参数,指在Izt=20mA时,稳压管的稳压值Vz=5.1V,也就是说流过稳压管的电流在20mA以上,稳压效果最佳。三极管导通之后,E点电压会升高,当升高到Vbe。

U_YAO的博客 2321

Hash冲突以及如何解决Hash冲突

一、哈希冲突 Hash冲突指的是在向Hash表中存数据时,首先要用Hash函数计算出该数据要存放的地址。但是在这个地址中已经有值存在,所以这个时候就发生了Hash冲突,

Funny54的博客 1万+

DSPACE仿真平台的使用

DSPACE仿真平台的使用 文章目录DSPACE仿真平台的使用1.ModelDesk新建项目2.MotionDesk引入项目3.ControlDesk引入项目4.项目整合运行 我们使用DSPACE仿真平台软件来模拟小汽车的自动驾驶。 总共需要用到DSPACE大类下的三个软件:ModelDesk、ControlDesk、MotionDesk。 1.ModelDesk新建项目 在桌面上点击ModelDesk图标,打开DSPACE—ModelDesk 选择 File 标签,侧边栏选择 New,点击 New A

qq_42582489的博客 7527

hash冲突(碰撞)及解决方法

文章目录一、什么是hash冲突?二、产生hash冲突的原因三、解决hash冲突的4中方式1.开放地址方法(1)线性探测(2)再平方探测(3)伪随机探测2.链式地址法(HashMap的哈希冲突解决方法)3.建立公共溢出区4.再哈希法 一、什么是hash冲突? 就是根据key即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经有人先来了 就是相当于你在蹲坑,又有人进来了,但是只有一个坑,就产生了冲突,需要开辟新的空

weixin_43716329的博客 8738

开关电源EMI优化秘籍:三明治绕法如何让你的产品通过认证测试

本文深入解析了开关电源EMI优化中的三明治绕法,详细阐述了其通过改变绕组排布来减小初次级寄生电容、降低共模噪声的核心机理。文章提供了从绕制结构选择、绝缘安规处理到实测验证的全流程实战指南,帮助工程师有效利用这一工艺,提升产品通过CCC、CE等EMI认证测试的成功率。

tcp8optimizer的博客 705

卡诺图化简法注意事项+竞争冒险知识点总结

今天反思以下卡诺图化简法中的注意事项。 卡诺图看起来简单,但如果不熟悉的话很容易出错,因此要对画圈的规则比较熟悉。

weixin_42415266的博客 1万+

什么是Hash冲突?如何解决Hash冲突?

1.Hash Hash叫做”散列表“,就是把任意长度的输入,通过散列算法,变成固定长度输出,该输出结果是散列值。 其实这种转换是一种压缩映射,散列表的空间通常小于输入的空间,不同的输入可能会散列成相同的输出,所以不能从散列表来唯一的确定输入值。这就出现了Hash冲突。 Hash冲突: 根据key(键)即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经被占用了。这就是所谓的hash冲突。 2.解决Hash冲

weixin_49097844的博客 2万+

GAN网络详解析

介绍目前常见的GAN网络,以及GAN网络的基本组成

wm199的博客 6504

什么是 Hash 冲突?如何解决 Hash 冲突?

HashMap基于Hash算法实现的,采用数组的结果存储数据,数组长度的默认长度为16,我们通过put(key,value)存储,get(key)来获取。当传入key时,HashMap会根据key,hashCode()计算出hash值,根据hash值将value保存bucket里。当计算出的hash值相同时,我们称之为hash冲突。......

qq_53946134的博客 2203

USB-C快充协议分析仪:QC/PD双协议识别与功率监测设计

USB-C快充协议是现代移动设备能量传输的核心技术标准,其本质是通过物理层电压特征(如DP/DM组合、CC线BMC编码)实现动态电压协商。理解协议握手原理对充电器研发、兼容性测试及故障诊断至关重要。本方案聚焦受电端(Sink)视角,基于FUSB302B与高阻抗DP/DM采样电路,实现QC2.0/QC3.0/QC4+与PD2.0/PD3.0的实时识别与状态机解析;结合INA219高精度电流检测与μs级同步采样,达成±0.06%功率测量精度。典型应用于快充芯片验证、产线协议一致性测试及第三方评测,为USB供电系

weixin_42153793的博客 244

LSTM 深度解析从原理推演到现代序列建模的演进

LSTM(Long Short-Term Memory,长短期记忆网络)是一种特殊的递归神经网络(RNN),它通过引入记忆单元(Cell State)来解决普通RNN在长序列数据处理中的梯度消失问题。LSTM可以在长时间的序列中捕捉依赖关系,是一种非常适合处理时间序列、自然语言处理、语音识别等任务的深度学习模型。在LSTM中,记忆单元(Cell State)是一个贯穿整个序列的数据通道,用于存储和传递关键信息。记忆单元在LSTM的时间步之间保持信息的持久性,可以看作是一个长期记忆的“容器”。

hello.reader 3万+

深入理解 HashMap:从基础到源码,全面解析 Java 中的哈希表实现

本文全面解析Java中HashMap的实现原理,从基础用法到源码分析。HashMap是基于哈希表实现的键值对数据结构,具有常数时间复杂度的基本操作。文章详细对比了JDK 1.7和1.8版本的改进,包括红黑树优化、尾插法避免死循环等。通过源码分析核心组件如Node类、哈希函数和扩容机制,深入剖析HashMap的底层实现。同时提供了性能优化建议、线程安全替代方案以及常见面试问题解答,帮助开发者全面掌握这一Java集合框架中最重要的数据结构。

千淘万漉虽辛苦,吹尽狂沙始到金 2万+

PCL Fast Global Registration 快速全局配准(源码版)

Fast Global Registration(FGR)是一种经典的点云粗配准算法,已集成到Open3D和CloudCompare等软件中。本文介绍了该算法的源码实现及优化方法,包括如何通过修改源码直接读取点云进行配准,而不依赖文件中读取特征描述子。文章还提供了关键参数设置(如最大迭代次数、匹配距离等)。算法实现简单高效,未来计划发布基于纯PCL的版本。源码和论文链接已附上,供开发者参考。

点云侠的博客 383

HashMap底层原理

众所周知,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。 HashMap数组每一个元素的初始值都是Null。 对于HashMap,我们最常使用的是两个方法:Get 和 Put。 1.Put方法的原理 调用Put方法的

m0_37133390的博客 207

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

Rust HashMap深度剖析:从哈希算法到冲突解决的工程实践

本文深入探讨Rust标准库HashMap的内部实现机制,从哈希算法选择、冲突解决策略到性能优化实践,结合真实生产场景分析HashMap的设计哲学与工程权衡。

m0_74919787的博客 1034

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

HashMap源码分析

0.Hash算法 常见的Hash算法 直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。 数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。 除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。 分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。 平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。

Zeroowt的博客 231

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

什么时候ReHash,HashMap的内部实现机制,Hash是怎样实现的 - schbook

原文:http://www.cnblogs.com/schbook/p/3585159.html?utm_source=tuicool&utm_medium=referral 1.HashMap的内部实现机制 HashMap是对数据结构中哈希表(Hash Table)的实现, Hash表又叫散列表。Hash表是根据关键码Key来访问其对应的值Value的数据结构,它通过一个

SoundSlow的博客 1200

HashMap的内部实现机制,Hash是怎样实现的,什么时候ReHash

1.HashMap的内部实现机制 HashMap是对数据结构中哈希表(Hash Table)的实现,Hash表又叫散列表。Hash表是根据关键码Key来访问其对应的值Value的数据结构,它通过一个映射函数把关键码映射到表中一个位置来访问该位置的值,从而加快查找的速度。这个映射函数叫做Hash函数,存放记录的数组叫做Hash表。 在Java中,HashMap的内部实现结合了链表和数组的优势,链...

weixin_30432179的博客 60

HashMap的底层原理

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。(底层是Entry数组+链表(每个Entry里面会形成)) HashMap数组每一个元素的初始值都是Null。   对于HashMap,我们最常使用的是两个方法:Get 和 Put。 1.Put方法的原理 调用P...

ycmnzmm的博客 520

hash原理、冲突以及解决方法

在Java编程语言中,最基本的结构就是两种,一种是数组,一种是模拟指针(引用),所有的数据结构都可以用这两个基本结构构造,HashMap也一样。当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例: HashMap<String,Object> m=new HashMap<String,Object>(); m.put("a", "rrr1"); m.put("b", "tt9"); m.put("c", "tt8"); m.put

GMH的博客 370

ThreadLocal 底层实现原理解析

ThreadLocal的底层实现是一套精巧的线程隔离机制,其核心在于ThreadLocalMap数据结构、斐波那契散列算法和弱引用内存管理。

weixin_44636027的博客 2251

Java中HashMap的实现原理

HashMap是现在面试中最受欢迎的话题。 假设如果你对HashMap的内部工作感兴趣。在向前推进之前,我强烈建议您阅读我以前的文章:在java中使用hashCode和equals方法。 单一声明答案 如果有人要求我描述“ HashMap如何工作?“我只是回答:” 哈哈的原则 “。一样简单。我们必须非常肯定地了解至少哈希的基础知识。 什么是哈希?

_William_Cheung的博客 475

面试官问:你说一说 HashMap 是如何解决 hash 冲突的?

>>号外:关注“Java精选”公众号,菜单栏->聚合->干货分享,回复关键词领取视频资料、开源项目。在Java编程语言中,最基本的结构就是两种,一种是数组,一种是模...

Java精选 404

HashMap剖析

HashMap剖析 这两天对HashMap做了个回顾再吸收,烦请各路大佬指教。 HashMap的实现原理 1.HashMap基于哈希原理,通过put(key, value)和get(key)方法存储和获取对象; 2.当存储对象时,将键值对传递给put(key value)方法时,它调用键对象key的hashCode()方法来计算hashcode,然后找到bucket位置,来存储值对象value; 3.当获取对象时,也是先计算key的hashCode,找到数组中对应位置的bucket位置,然后通过key的eq

PanTianTianO的博客 839

Java面试题--HashMap是什么?

众所周知,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。HashMap数组每一个元素的初始值都是Null 对于HashMap,我们最常使用的是两个方法:Get 和 Put。 1.Put方法的原理调用Put方法的时候发生了什么呢?比如调用 hashMap.put(“a

炫的博客 615

java.util.HashMap源码解析

1.

sunpy 1101
上一篇: 合并排序解析
下一篇: java中String类型参数传递问题
coderLee0607
博客等级 码龄14年 88粉丝 75原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值