看C#与JAVA源码时发现C#与JAVA哈希表的实现略有不同,特此分享一下。


基本结构比较

JAVA的桶

278     static class Node<K,V> implements Map.Entry<K,V> {279         final int hash;
280         final K key;
281         V value;
282         Node<K,V> next;
283
284         Node(int hash, K key, V value, Node<K,V> next) {285             this.hash = hash;
286             this.key = key;
287             this.value = value;
288             this.next = next;
289         }
290
291         public final K getKey()        { return key; }
292         public final V getValue()      { return value; }
293         public final String toString() { return key + "=" + value; }
294
295         public final int hashCode() {296             return Objects.hashCode(key) ^ Objects.hashCode(value);
297         }
298
299         public final V setValue(V newValue) {300             V oldValue = value;
301             value = newValue;
302             return oldValue;
303         }
304
305         public final boolean equals(Object o) {306             if (o == this)
307                 return true;
308             if (o instanceof Map.Entry) {309                 Map.Entry<?,?> e = (Map.Entry<?,?>)o;
310                 if (Objects.equals(key, e.getKey()) &&
311                     Objects.equals(value, e.getValue()))
312                     return true;
313             }
314             return false;
315         }
316     }

C#的桶

        private struct bucket {public Object key;public Object val;public int hash_coll;   // Store hash code; sign bit means there was a collision.}

桶PK结果

乍一看差很多,JAVA是引用类型,C#是值类型。但从空间角度来看实际上差不多,C#的桶虽然是值类型,但是它基本是装箱后作为哈希表对象的成员存在于堆之中。JAVA是三个引用加一个int,JAVA的桶里有几个方法,占一些代码段空间并在堆中应该会占用几个方法指针空间,C#是两个引用加一个int,C#略小。虽然C#桶较小,但是C#每个桶只能最多放一个元素,JAVA的桶存放的是链表或树,一般情况下C#整个哈希表的堆空间暂用要大于JAVA,并且随着被存储的元素数量增加,这个差距还要大幅上升。


装载系数比较

load factor装载系数决定哈希表何时扩容。例如load factor=0.5,那么当有一半桶都存有元素时,哈希表就会进行扩容。对C#来说,装载系数直接影响桶的数量,对JAVA来说更影响的是查询速度,因为高装载系数会容忍更多的碰撞,也就影响桶内的链表长度和树的深度。

C#的默认装载系数=0.72

>             // Based on perf work, .72 is the optimal load factor for this table.  this.loadFactor = 0.72f * loadFactor;

C#关于装载系数的说明:
…Smaller load factors cause faster average lookup times at the cost of increased memory consumption. A load factor of 1.0 generally provides the best balance between speed and size. …
装载系数1.0可以达到内存空间占用与查找时间的最佳平衡,越小的装载系数会缩短查询时间但也会加大内存占用。

JAVA的默认装载系数=0.75:

Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75).
473
474     public HashMap() {475         this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
476     }

JAVA源码注释,关于装载系数的解释:
An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
哈希表的实例有两个参数会印象它的性能:初始容量和极限装载值。容量是哈希表桶的数量,初始容量既是哈希表创建时桶的数量。极限装载值决定哈希表何时扩容。当入口数量超过极限装载值与当前容量的乘数时,哈希表将会重建并扩容至将近两倍。

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.
一般说来,默认的极限装载系数(0.75)提供一个空间与时间上的良好平衡。更大的极限装载系数将会降低空间占用但是会增加查询时间(反应在绝大部分哈希表方法中,包括get和put)。在设置初始容量前应考虑实际的入口数量和表的极限装载系数,以降低哈希表扩容的次数。如果初始容量大于入口的最大可能数量除以极限装载系数,哈希表将永远不会进行重建操作。

装载系数PK结果

看来两家都认为0.7x是一个比较好的值,同样的值但是意义上略有不同,0.7x的值对C#来讲更侧重查询速度,对JAVA更侧重与空间优化。


哈希函数比较

JAVA的哈希函数:

336     static final int hash(Object key) {337         int h;
338         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
339     }

比较简单,高低16位异或。
然后将哈希函数返回的值转换为桶的位置:桶数减1与哈希函数返回值进行&与运算。在算出哈希值后再进行桶位置计算:

index = (n - 1) & hash

这是一个掩码运算,将计算出的hash值截取为一个小于哈希表现有的桶的数量的一个值。例如,假设桶数n为16,n-1=15,则n-1的二进制值为00000000 00000000 00000000 00001111。任何一个数值与n-1位向量进行&运算,除了后四位以外前面都会变为0。

所以说决定一切的只是没有被掩码运算抹掉的最后几位,被归0的部分再长再复杂也都是不相干的,那么假设需要放进哈希表的一组元素正好位向量的后几位都相同岂不糟糕。高低16位异或就是为了降低键的哈希值最低位出现相同情况的概率。

C#的哈希函数:

        private uint InitHash(Object key, int hashsize, out uint seed, out uint incr) {uint hashcode = (uint) GetHash(key) & 0x7FFFFFFF;seed = (uint) hashcode;incr = (uint)(1 + ((seed * HashPrime) % ((uint)hashsize - 1)));return hashcode;}

掩码&x7FFFFFFF将seed最高五位置为0,用来记录是否发生碰撞,优化之后的查询速度。

取到哈希值后计算桶位置:

int bucketNumber = (int) (seed % (uint)lbuckets.Length);

seed是hashcode的uint版本,返回值转化为桶位置,取余既是掩码运算。

哈希函数PK结果

C#比JAVA更耗性能,seed,incr的赋值都需要访存,hashcode,seed,incr有数据相关,有几个uint的显示类型转换,相比JAVA多计算了一个incr。相反JAVA版本异常简洁,两个位运算加一个三元操作,代码优化应该已经到极致了。


碰撞处理比较

C#发生碰撞后的处理

为什么C#要算一个incr呢?因为它采取的是一种称为“双重哈希”的机制,每次进入哈希函数的时候会保存一个incr值,这实际上是一个“第二哈希函数值”,当计算出桶位置后发生碰撞再次调用哈希函数寻找下一个桶时它就派上了用场。
当发生碰撞后,寻找下一个桶的位置:

bucketNumber = (int) (((long)bucketNumber + incr)% (uint)lbuckets.Length);

这行代码是放在一个循环中的,所以incr也可以看做一个增量,通过一个增量去寻找哈希表的下一个空桶的方法也叫“开放寻址法”。例如“开放寻址法”中的“线性探索”,第一次通过哈希函数计算出桶的位置为5,访问第5个桶,发现此桶已有人了,那么下一次访问第6个桶,直到发现空桶为止。双重哈希既是把线性探索中的增量变为了“第二哈希函数”的返回值:incr = (uint)(1 + ((seed * HashPrime) % ((uint)hashsize - 1))); 每次检测出碰撞后都由前一个桶的位置加此增量,直至找到空桶为止。incr的算法会对找到空桶的速度有影响。

JAVA发生碰撞后的处理

通过PUT方法看一下JAVA对碰撞的处理。

624     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
625                    boolean evict) {626         Node<K,V>[] tab; Node<K,V> p; int n, i;
627         if ((tab = table) == null || (n = tab.length) == 0)
628             n = (tab = resize()).length;
629         if ((p = tab[i = (n - 1) & hash]) == null)
630             tab[i] = newNode(hash, key, value, null);
631         else {632             Node<K,V> e; K k;
633             if (p.hash == hash &&
634                 ((k = p.key) == key || (key != null && key.equals(k))))
635                 e = p;
636             else if (p instanceof TreeNode)
637                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
638             else {639                 for (int binCount = 0; ; ++binCount) {640                     if ((e = p.next) == null) {641                         p.next = newNode(hash, key, value, null);
642                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
643                             treeifyBin(tab, hash);
644                         break;
645                     }
646                     if (e.hash == hash &&
647                         ((k = e.key) == key || (key != null && key.equals(k))))
648                         break;
649                     p = e;
650                 }
651             }
652             if (e != null) { // existing mapping for key
653                 V oldValue = e.value;
654                 if (!onlyIfAbsent || oldValue == null)
655                     e.value = value;
656                 afterNodeAccess(e);
657                 return oldValue;
658             }
659         }
660         ++modCount;
661         if (++size > threshold)
662             resize();
663         afterNodeInsertion(evict);
664         return null;
665     }

当发生碰撞后,桶内原有节点变为一个链表头,将新键值通过链表连接到现有节点后面(641行),当链表深度超过一个阀值后会将整个哈希表变为一个树(642行),变为树后再发生碰撞,就会将新键值在树中进行连接(637行)。

JAVA哈希表中的树:

1791    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {1792        TreeNode<K,V> parent;  // red-black tree links
1793        TreeNode<K,V> left;
1794        TreeNode<K,V> right;
1795        TreeNode<K,V> prev;    // needed to unlink next upon deletion
1796        boolean red;
1797        TreeNode(int hash, K key, V val, Node<K,V> next) {1798            super(hash, key, val, next);
1799        }

看注释这是一个红黑树。

碰撞处理PK结果

时间上:
1,插入
JAVA发生碰撞后进行链表或树操作,有next节点,插入是很快的。而C#方面则是取决于双重哈希算法的效率,计算出的incr的合理性直接决定寻找空桶的速度,但无论如何在碰撞后都要最少计算一次下一个桶的位置,因此JAVA的链表指针赋值略块。
2,查询
JAVA查询发生碰撞时会对桶内的链表或树进行搜索,速度取决于链表或树的深度。C#要根据双重哈希与开放寻址法继续寻找下一个桶,正常来讲,遍历一个数组时,cpu会对其进行高速缓存,所以循环递增查询数组是快于链表和树的,但是由于C#哈希表桶数组的遍历要使用一个incr增量,在一个很长的桶数组和incr值较大的场景中,能否将要遍历的桶筛选出来并进行高速缓存取决于cpu的能力。

空间上:
C#碰撞越多,扩容几率越大,而JAVA相反,碰撞的元素都存在一个桶中,JAVA胜。


增容方法比较

C#的增容方法

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]private void rehash( int newsize, bool forceNewHashCode ) {// reset occupancyoccupancy=0;// Don't replace any internal state until we've finished adding to the // new bucket[].  This serves two purposes: //   1) Allow concurrent readers to see valid hashtable contents //      at all times//   2) Protect against an OutOfMemoryException while allocating this //      new bucket[].bucket[] newBuckets = new bucket[newsize];// rehash table into new bucketsint nb;for (nb = 0; nb < buckets.Length; nb++){bucket oldb = buckets[nb];if ((oldb.key != null) && (oldb.key != buckets)) {int hashcode = ((forceNewHashCode ? GetHash(oldb.key) : oldb.hash_coll) & 0x7FFFFFFF);                              putEntry(newBuckets, oldb.key, oldb.val, hashcode);}}// New bucket[] is good to go - replace buckets and other internal state.
#if !FEATURE_CORECLRThread.BeginCriticalRegion();
#endifisWriterInProgress = true;buckets = newBuckets;loadsize = (int)(loadFactor * newsize);UpdateVersion();isWriterInProgress = false;
#if !FEATURE_CORECLR            Thread.EndCriticalRegion();
#endif         // minimun size of hashtable is 3 now and maximum loadFactor is 0.72 now.Contract.Assert(loadsize < newsize, "Our current implementaion means this is not possible.");return;}

不复杂,新建一个略大的桶数组,遍历旧桶数组中每个桶,将旧桶中的东东通过哈希函数重新计算位置,放入新桶中。

JAVA的增容方法

676     final Node<K,V>[] resize() {677         Node<K,V>[] oldTab = table;
678         int oldCap = (oldTab == null) ? 0 : oldTab.length;
679         int oldThr = threshold;
680         int newCap, newThr = 0;
681         if (oldCap > 0) {682             if (oldCap >= MAXIMUM_CAPACITY) {683                 threshold = Integer.MAX_VALUE;
684                 return oldTab;
685             }
686             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
687                      oldCap >= DEFAULT_INITIAL_CAPACITY)
688                 newThr = oldThr << 1; // double threshold
689         }
690         else if (oldThr > 0) // initial capacity was placed in threshold
691             newCap = oldThr;
692         else {               // zero initial threshold signifies using defaults
693             newCap = DEFAULT_INITIAL_CAPACITY;
694             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
695         }
696         if (newThr == 0) {697             float ft = (float)newCap * loadFactor;
698             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
699                       (int)ft : Integer.MAX_VALUE);
700         }
701         threshold = newThr;
702         @SuppressWarnings({"rawtypes","unchecked"})
703             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
704         table = newTab;
705         if (oldTab != null) {706             for (int j = 0; j < oldCap; ++j) {707                 Node<K,V> e;
708                 if ((e = oldTab[j]) != null) {709                     oldTab[j] = null;
710                     if (e.next == null)
711                         newTab[e.hash & (newCap - 1)] = e;
712                     else if (e instanceof TreeNode)
713                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
714                     else { // preserve order
715                         Node<K,V> loHead = null, loTail = null;
716                         Node<K,V> hiHead = null, hiTail = null;
717                         Node<K,V> next;
718                         do {719                             next = e.next;
720                             if ((e.hash & oldCap) == 0) {721                                 if (loTail == null)
722                                     loHead = e;
723                                 else
724                                     loTail.next = e;
725                                 loTail = e;
726                             }
727                             else {728                                 if (hiTail == null)
729                                     hiHead = e;
730                                 else
731                                     hiTail.next = e;
732                                 hiTail = e;
733                             }
734                         } while ((e = next) != null);
735                         if (loTail != null) {736                             loTail.next = null;
737                             newTab[j] = loHead;
738                         }
739                         if (hiTail != null) {740                             hiTail.next = null;
741                             newTab[j + oldCap] = hiHead;
742                         }
743                     }
744                 }
745             }
746         }
747         return newTab;
748     }

源码注释中有写,扩容后,节点要么原地不动,否则移动到index+2次幂的位置,

Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.

怎么实现的呢?

711                         newTab[e.hash & (newCap - 1)] = e;

看711行,假设oldCap=16,newCap=32,e1.hash的位向量为-----01010(前面的位无意义,不考虑),那么

01010(e1.hash) &11111 (newCap-1) = 01010

e1原来的index为1010,新的index没有发生变化。假设e2.hash的位向量为—11010,那么

11010(e2.hash)&11111(newCap-1)=11010

e2原来的index为5,二进制1010,新的index为21,二进制11010,增加了16。

再考虑下,如果e1,e2原hash值相同,那么则是发生了碰撞,e2其实是e1的子节点,重新分配后,e1原地不动,而e2则分配到了oldIndex+16的位置上(见代码720行 ,if ((e.hash & oldCap) == 0),此处与711行e.hash & (newCap - 1)异曲同工)。通过这种机制,所有的节点以二分法再次平均的分配到了新的桶中。

增容方法PK结果

JAVA完胜C#,通过一次循环与位运算就进行了巧妙的完成了再次分配与优化,而C#每个节点都要重新进行哈希函数的运算,虽然JAVA在有树或链表的情况下需要重连链表,但是性能消耗上与C#数组元素赋值无太大区别。空间上,两家都是*2扩容。


总结

两家各有胜负,但实际上没什么意义,整体性能决定一切,而这需要用实验和数据来做判断,由于哈希表只是一个工具,在数据量和操作重心无法确定的情况下,实验也无法得出一个适合所有场景的普遍结论,很有可能是不同环境下各胜一筹。此文主要还是展示两种语言对同一工具的不同实现方法,都说C#是山寨JAVA,也许这话是说在架构的层面,在源码实现层面上,C#的双重哈希与JAVA的链表转红黑树各有各的巧妙,其实在设计上也算是泾渭分明的。


附录:

一个自制的简易哈希表(初始容量为16,极限装载值为0.75,使用链表处理碰撞,哈希函数使用高低16位异或,暂无扩容方法,无删除方法,不支持null值。):

/* Script name: LiuHashMap.cs* Created on: 16-2-2017* Author: liu_if_else* Purpose: This is a simple version of HashMap. Demonstrates some basic functions of the collection type. * History: 21-2-2017, improved the code.
*/using System.Collections.Generic;
using System;public class LiuHashMap<K,V> {//哈希表类内嵌套类,用作记录键值得节点class Liu_node<K,V>{public K key;public V value;//链表指针public Liu_node<K,V> next;//节点构造函数public Liu_node(K key,V value){this.key=key;this.value=value;}}//初始容量private int initial_capacity=16;//极限装载值private float load_factor=0.75f;//节点数组,既是桶private Liu_node<K,V>[] liu_nodes;//当前最大容许入口数量private int threshold;//当前入口数量private int entries_number = 0;//极限入口数量,32位int的最大值private int MAXIMUM_ENTRIES=2147483647;//构造函数public LiuHashMap():this(16,0.75f){}public LiuHashMap(int capacity,int loadFactor){if (capacity < 0) throw new ArgumentOutOfRangeException(); if (!(loadFactor >= 0.1f && loadFactor <= 1.0f))throw new ArgumentOutOfRangeException(); threshold = capacity * loadFactor;liu_nodes=new Liu_node<K,V>[capacity];}//add键值方法public void Add(K key,V value){Liu_node<K,V> current_node = new Liu_node<K,V> (key, value);//通过哈希函数计算放置此键值的桶的indexint hashcode = hash (key);int index = hashcode & (initial_capacity-1);//如果此桶处数组为空,则将键值放入此处if (liu_nodes [index] == null) {liu_nodes [index] = current_node;} //如果此桶已经有键值,则是发生了碰撞,通过链表将键值链接在此处节点的next处else {liu_nodes [index].next = current_node;return;}//入口数量加1;entries_number += 1;//如果入口数量大于了最大容许入口数量,则进行扩容if (entries_number > threshold) {Resize ();}}//取值方法public V GetValue(K key){//通过要搜索的键计算桶的位置int hashcode = hash (key);int index = hashcode & initial_capacity;//取键值Liu_node<K,V> current_node = liu_nodes [index];//检查此桶是否还有多个节点,既检查碰撞while (current_node != null) {//通过Equals方法寻找正确的键if(current_node.key.Equals(key)){return current_node.value;}//遍历链表current_node = current_node.next;   }return default(V);}//检查是否存在此key,与上同理public bool Contains(K key){int hashcode = hash (key);int index = hashcode & initial_capacity;Liu_node<K,V> current_node = liu_nodes [index];while (current_node != null) {if(current_node.key.Equals(key)){return true;}//遍历链表current_node = current_node.next;   }return false;}//扩容方法,暂无private void Resize(){//如果扩容后的容量会超出极限容量值则放弃扩容if (initial_capacity * 2 > MAXIMUM_ENTRIES) {return;}//扩容方法,暂略}//哈希函数private int hash(K key){//这里使用Java的高低16位异或int h;return(key==null)?0:(h=key.GetHashCode())^(h>>16);}
}

参考:
C#哈希表源码地址:
https://referencesource.microsoft.com/#mscorlib/system/collections/hashtable.cs,adc3a33902ee081a
JAVA哈希表源码地址:
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/HashMap.java

维护日志:
2020-8-14:review

Code Review:C#与JAVA的哈希表内部机制的一些区别相关推荐

  1. Java,哈希码以及equals和==的区别(转载)

    从开始学习Java,哈希码以及equals和==的区别就一直困扰着我.     要想明白equals和==的区别首先应该了解什么是哈希码,因为在jdk的类库中不管是object实现的equals()方 ...

  2. Java,哈希码以及equals和==的区别

    从开始学习Java,哈希码以及equals和==的区别就一直困扰着我.要想明白equals和==的区别首先应该了解什么是哈希码,因为在jdk的类库中不管是object实现的equals()方法还是St ...

  3. 在java中哈希表判断某个元素是否存在的原理

    在java中哈希表判断某个元素是否存在的原理 在本文中我们将介绍,在hash表中如何判断两个元素是否重复. 首先,我们们需要知么hash表是什么? Hash表由称为离散表,是由数组加链表实现的一种数据 ...

  4. JAVA中哈希表的使用-遍历map集合

    java中哈希表的使用第二例-即将罗马数字转换为整数 代码: class Solution { public int romanToInt(String s) { HashMap<Charact ...

  5. java中哈希表怎么表示_java中HashMap概念是什么?怎么存取实现它?

    时代总是在不断的变化发展的,高新技术的应用也越来越普遍,大家对于新知识的渴望越来越强烈.java中很多的基础知识都是非常重要的.一起来看看关于HashMap的知识吧. 一. HashMap概述: Ha ...

  6. 数据结构(Java)-哈希表

    哈希表 1. 基本概念 哈希表是一种根据关键码去寻找值的数据映射结构,该结构通过把关键码映射的位置去寻找存放值的地方,说起来可能感觉有点复杂,我想我举个例子你就会明白了,最典型的的例子就是字典,大家估 ...

  7. 在java中 哈希表会经常出现哈希碰撞吗

    在Java中,哈希表可能会经常出现哈希碰撞.哈希表是一种根据键(Key)来访问值(Value)的数据结构,通过哈希函数将键映射到哈希表的索引位置上.由于哈希函数的映射结果可能不唯一,不同的键可能会被映 ...

  8. java中哈希表HashMap详解遍历操作

    一.主题外谈 在进入主题之前,先讲一点与哈希表相关的一些东西: 1.Hash算法, 通常还可用作快速查找. 2.哈希函数,是支撑哈希表的一类「函数」. 3.哈希表(Hash Table),是一种**「 ...

  9. LeetCode笔记】剑指 Offer 35. 复杂链表的复制(Java、哈希表、原地算法)

    文章目录 题目描述 思路 && 代码 1. 哈希表法 2. 原地算法 二刷 题目描述 主要有两个考虑点: 不能改变原链表 新链表赋予 next.random 时,复制结点不一定存在 思 ...

最新文章

  1. php mysql 备注_php,mysql备注信息1
  2. 客户端渲染 服务端渲染_这就是赢得客户端渲染的原因
  3. python中pygal_Python数据可视化之Pygal图表类型
  4. 2018新版正方教务 ---爬虫--- JAVA源码--课表--平时分----成绩-----排名----考试安排...
  5. java 获取 邮箱联系人_在android中读取联系人信息的程序,包括读取联系人姓名、手机号码和邮箱...
  6. 关于使用安装Adobe绿色精简版所需运行库
  7. JDK源码解析之 java.lang.Integer
  8. 编写一个程序求输入字符串的长度
  9. 怎么只要小数部分C语言,如何得出一个浮点数的小数部分,要把各个位保存到一个数组里边。...
  10. 币安Binance API
  11. 【流量池】裂变营销:10种人脉裂变技能,6个裂变核心,8个吸粉人性本能怎样玩粉丝裂变?
  12. excel power Query
  13. PPT文件太大怎么办?如何压缩PPT?这几招帮你搞定
  14. 学术论坛第三期:多指标异常检测方法综述
  15. openstack compute service list报错(HTTP 503)
  16. 【地图可视化】Echarts地图上展示3D柱体
  17. 【ffmpeg】curl : m3u8 to mkv
  18. 微信小程序踩坑—用户登录界面
  19. 【深度学习/机器学习】为什么要归一化?归一化方法详解
  20. TextView和EditText

热门文章

  1. Spark 性能相关参数配置详解-压缩与序列化篇
  2. Spark 部署及示例代码讲解
  3. Spark SQL连接数据库找不到Mysql驱动解决方法
  4. postman本地请求ip地址变成ipv6_华为认证-IPv6技术-ICMPv6介绍
  5. 【pytest】Hook 方法之 pytest_collection_modifyitems:修改测试用例执行顺序
  6. 下载HTMLTestRunner如何使用
  7. Linux卸载搜狐,搜狐的linux笔试题
  8. cadence 常见pcb电阻_高速PCB培训手记
  9. kitti数据集_神秘的Waymo一反常态,CVPR现场发布大型自动驾驶数据集
  10. mysql float 怎么设置长度_MySQL中float double decimal区别总结