C#要点技术(二) - Dictionary 底层源码剖析

news2024/11/29 12:54:08

Dictionary 底层代码

我们知道 Dictionary 字典型数据结构,是以关键字Key 和 值Value 进行一一映射的。Key的类型并没有做任何的限制,可以是整数,也可以是的字符串,甚至可以是实例对象。关键字Key是如何映射到内存的呢?

其实没有什么神秘的,这种映射关系可以用一个Hash函数来建立,Dictionary 也确实是这样做的。这个Hash函数也并非神秘,我们可以简单的认为它只是做了一个模(Mod余)的操作,Dictionary 将每个Key加入容器的元素都要进行一次Hash哈希的运算操作,从而找到自己的位置。

Hash函数可以有很多种算法,最简单的可以认为是余操作,比如当Key为整数93时

hash_key = Key % 30 = 3

对于对象和字符串来说,虽然没有直接点数字做标准,但也能以实例ID为标准来做Hash操作。实际算法可能没有我举例子这么简单,我们将在下面的源码剖析中详细讲解。

对于不同的关键字可能得到同一哈希地址,即

key1 != key2 => F(key1)=F(fey2)

这种现象叫做Hash哈希冲突,在一般情况下,冲突只能尽可能的少,而不能完全避免。因为哈希函数是从关键字范围到索引范围的映射,通常关键字范围要远大于索引范围,它的元素包括多个可能的关键字。既然如此,如何处理冲突则是构造哈希表不可不解决的一个问题。

在处理Hash哈希冲突的方法中通常有:开放定址法、再哈希法、链地址法、建立一个公共溢出区等。Dictionary使用的解决冲突方法是拉链法,又称链地址法。

拉链法的原理:

将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为n,则可将散列表定义为一个由n个头指针组成的指针数 组T[0..n-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。

在哈希表上进行查找的过程,和,在哈希表构建的过程是基本一致的。

给定Key值,根据造表时设定的哈希函数求得哈希地址,若表中此位置没有记录,则查找不成功;否则比较关键字,若何给定值相等,则查找成功;否则根据处理冲突的方法寻找“下一地址”,直到哈希表中某个位置为空或者表中所填记录的关键字等于给定值时为止。

我们来看看更形象的结构图,如下:

如图所示,拉链法结构中,主要的宿主为数组指针,每个数组元素里存放着指向下一个节点的指针,如果没有元素在单元上,则为空指针。当多个元素都指向同一个单元格时,则以链表的形式依次存放并列的元素。

在 Dictionary 中究竟是如何实现的呢,我们来剖析一下源码。

首先我们来看看源码中对 Dictionary 的变量定义部分,如下:


public class Dictionary<TKey,TValue>: IDictionary<TKey,TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback 
{
    
    private struct Entry {
        public int hashCode;    // Lower 31 bits of hash code, -1 if unused
        public int next;        // Index of next entry, -1 if last
        public TKey key;           // Key of entry
        public TValue value;         // Value of entry
    }

    private int[] buckets;
    private Entry[] entries;
    private int count;
    private int version;
    private int freeList;
    private int freeCount;
    private IEqualityComparer<TKey> comparer;
    private KeyCollection keys;
    private ValueCollection values;
    private Object _syncRoot;
}

从继承的类和接口看,Dictionary 主要继承了 IDictionary 接口,和 ISerializable 接口。IDictionary 和 ISerializable 在使用过程中,其主要的接口为,Add, Remove, ContainsKey, Clear, TryGetValue, Keys, Values, 以及[]数组符号形式作为返回值的接口。也包括了常用库 Collection 中的接口,Count, Contains等。

从 Dictionary 的定义变量中可以看出,Dictionary 是以数组为底层数据结构的类。当我们实例化 new Dictionary() 后,内部的数组是0个数组的状态。与 List 组件一样,Dictionary 也是需要扩容的,会随着元素数量的增加而不断扩容。具体我们来看看下面的接口源码剖析。

下面的我们将围绕上述的接口进行解析 Dictionary 底层运作机制。

了解Add是最直接了解底层数据结构如何运作的途径,我们来看下Add接口的实现。

源代码如下:


public void Add(TKey key, TValue value)
{
    Insert(key, value, true);
}

private void Initialize(int capacity)
{
    int size = HashHelpers.GetPrime(capacity);
    buckets = new int[size];
    for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
    entries = new Entry[size];
    freeList = -1;
}

private void Insert(TKey key, TValue value, bool add)
{
    if( key == null ) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets == null) Initialize(0);
    int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    int targetBucket = hashCode % buckets.Length;

#if FEATURE_RANDOMIZED_STRING_HASHING
    int collisionCount = 0;
#endif

    for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
        if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
            if (add) { 
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
            }
            entries[i].value = value;
            version++;
            return;
        } 

#if FEATURE_RANDOMIZED_STRING_HASHING
        collisionCount++;
#endif
    }
    int index;
    if (freeCount > 0) {
        index = freeList;
        freeList = entries[index].next;
        freeCount--;
    }
    else {
        if (count == entries.Length)
        {
            Resize();
            targetBucket = hashCode % buckets.Length;
        }
        index = count;
        count++;
    }

    entries[index].hashCode = hashCode;
    entries[index].next = buckets[targetBucket];
    entries[index].key = key;
    entries[index].value = value;
    buckets[targetBucket] = index;
    version++;

#if FEATURE_RANDOMIZED_STRING_HASHING

#if FEATURE_CORECLR
    // In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
    // in this case will be EqualityComparer<string>.Default.
    // Note, randomized string hashing is turned on by default on coreclr so EqualityComparer<string>.Default will 
    // be using randomized string hashing

    if (collisionCount > HashHelpers.HashCollisionThreshold && comparer == NonRandomizedStringEqualityComparer.Default) 
    {
        comparer = (IEqualityComparer<TKey>) EqualityComparer<string>.Default;
        Resize(entries.Length, true);
    }
#else
    if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
    {
        comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
        Resize(entries.Length, true);
    }
#endif // FEATURE_CORECLR

#endif

}

展示的代码稍稍多了点,我们摘出其中的要点,通过要点来了解重点,再通过重点了解全局。

其实 Add 接口就是 Insert 的代理,因为它 Insert 一句话,那么 Inset 里做了什么呢?

首先在加入数据前需要对数据结构进行构造。

if (buckets == null) Initialize(0);

其实在 Dictionary 构建时如果没有指定任何数量 buckets 就有可能是空的,所以需要对buckets进行初始化,Initialize(0),说明构建的数量级最少。

不过奥妙就在 Initialize 函数里,如果传入的参数不是0,而是5、10、25、或其他更大的数量的话,那么构造多大的数据结构才合适呢?

在 Initialize 函数中,给了我们答案,看下面这行:

int size = HashHelpers.GetPrime(capacity);

它们有专门的方法来计算到底该使用多大的数组,我们查出源码 HashHelpers 中,primes数值是这样定义的:


 public static readonly int[] primes = {
        3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
        1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
        17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
        187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
        1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369};

public static int GetPrime(int min) 
{
    if (min < 0)
        throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));
    Contract.EndContractBlock();

    for (int i = 0; i < primes.Length; i++) 
    {
        int prime = primes[i];
        if (prime >= min) return prime;
    }

    //outside of our predefined table. 
    //compute the hard way. 
    for (int i = (min | 1); i < Int32.MaxValue;i+=2) 
    {
        if (IsPrime(i) && ((i - 1) % Hashtable.HashPrime != 0))
            return i;
    }
    return min;
}

// Returns size of hashtable to grow to.
public static int ExpandPrime(int oldSize)
{
    int newSize = 2 * oldSize;

    // Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow.
    // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
    if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize)
    {
        Contract.Assert( MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength");
        return MaxPrimeArrayLength;
    }

    return GetPrime(newSize);
}

上述代码为 HashHelpers 部分的源码,其中 GetPrime 会返回一个需要的 size 最小的数值,从 GetPrime 函数的代码中,我们可以知道这个 size 是由数组 primes 里的值与当前需要的数量大小有关,当需要的数量小于 primes 某个单元格的数字时返回该数字,而 ExpandPrime 则更加简单粗暴,直接返回原来size的2倍作为扩展数量。

从Prime的定义看的出,首次定义size为3,每次扩大2倍,也就是,3->7->17->37->…. 底层数据结构的大小是按照这个数值顺序来扩展的,除非你在创建 Dictionary 时,先定义了他的初始大小,指定的初始大小也会先被 GetPrime 计算该分配的数量最终得到应该分配的数组大小。这和 List 组件的分配方式一模一样。

我们继续看初始化后的内容,对关键字 Key 做Hash哈希操作从而获得地址索引:

    int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
    int targetBucket = hashCode % buckets.Length;

当调用函数获得Hash哈希值后,还需要对哈希地址做余操作,以确定地址落在 Dictionary 数组长度范围内不会溢出。

紧接着对指定数组单元格内的链表元素做遍历操作,找出空出来的位置将值填入。

for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
    if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
        if (add) { 
            ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
        }
        entries[i].value = value;
        version++;
        return;
    } 

#if FEATURE_RANDOMIZED_STRING_HASHING
    collisionCount++;
#endif
}

这一步就是前面我们所说的拉链法的链表推入动作。当获得Hash值的数组索引后,我们知道了该将数据存放在哪个数组位置上,如果该位置已经有元素被推入,则需要将其推入到链表的尾部。从for循环开始,检查是否到达链表的末尾,最后将数据放入尾部,并结束函数。

如果数组的空间不够了怎么办?源码中体现了这一点:

int index;
if (freeCount > 0) {
    index = freeList;
    freeList = entries[index].next;
    freeCount--;
}
else {
    if (count == entries.Length)
    {
        Resize();
        targetBucket = hashCode % buckets.Length;
    }
    index = count;
    count++;
}

entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = value;
buckets[targetBucket] = index;

当被用来记录剩余单元格数量的变量 freeCount 等于0时,则进行扩容,扩容后的大小就是我们前面提到的 调用 ExpandPrime 后的数量,即通常情况下为原来的2倍,再根据这个空间大小数字调用 GetPrime 来得到真正的新数组的大小。

了解了Add接口,我们来看看Remove部分。

删除的过程和插入的过程比较相似,因为要查找到Key元素所在位置,所以再次将Key值做哈希操作也是难免的,然后类似沿着拉链法的模式寻找与关键字匹配的元素。

Remove 用关键字删除元素的接口源码:

public bool Remove(TKey key)
{
    if(key == null) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets != null) {
        int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
        int bucket = hashCode % buckets.Length;
        int last = -1;
        for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next) {
            if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
                if (last < 0) {
                    buckets[bucket] = entries[i].next;
                }
                else {
                    entries[last].next = entries[i].next;
                }
                entries[i].hashCode = -1;
                entries[i].next = freeList;
                entries[i].key = default(TKey);
                entries[i].value = default(TValue);
                freeList = i;
                freeCount++;
                version++;
                return true;
            }
        }
    }
    return false;
}

我们注意到 Remove 接口相对 Add 接口简单的多,同样用哈希函数 comparer.GetHashCode 再除余后得到范围内的地址索引,再做余操作确定地址落在数组范围内,从哈希索引地址开始,查找冲突的元素的Key是否与需要移除的Key值相同,相同则进行移除操作并退出。

注意源码中,Remove 的移除操作并没有对内存进行删减,而只是将其单元格置空,这是位了减少了内存的频繁操作。

我们继续剖析另一个重要的接口 ContainsKey 检测是否包含关键字的接口。源码如下:

public bool ContainsKey(TKey key)
{
    return FindEntry(key) >= 0;
}

private int FindEntry(TKey key)
{
    if( key == null) {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }

    if (buckets != null) {
        int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
        for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
            if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
        }
    }
    return -1;
}

从源码中看到 ContainsKey 是一个查找Key位置的过程。它调用了 FindEntry 函数,FindEntry 查找Key值位置的方法跟我们前面提到的相同。从用Key值得到的哈希值地址开始查找,查看所有冲突链表中,是否有与Key值相同的值,找到即刻返回该索引地址。

有了前面对几个核心接口理解的基础,其他接口相对比较就简单多了,我们快速的看过去。
TryGetValue 尝试获取值的接口:

public bool TryGetValue(TKey key, out TValue value)
{
    int i = FindEntry(key);
    if (i >= 0) {
        value = entries[i].value;
        return true;
    }
    value = default(TValue);
    return false;
}

与 ContainsKey 同样,他调用的也是FindEntry的接口,来获取Key对应的Value值。

对[]操作符的重定义,源码:

public TValue this[TKey key] {
    get {
        int i = FindEntry(key);
        if (i >= 0) return entries[i].value;
        ThrowHelper.ThrowKeyNotFoundException();
        return default(TValue);
    }
    set {
        Insert(key, value, false);
    }
}

在重新定义[]符号的代码中,获取元素时也同样使用 FindEntry 函数,而 Set 设置元素时则使用与 Add 调用相同的 Insert函数,它们都是同一套方法,即哈希拉链冲突解决方案。

从源码剖析来看,哈希冲突的拉链法贯穿了整个底层数据结构。因此哈希函数是关键了,哈希函数的好坏直接决定了效率高低。

既然这么重要,我们来看看哈希函数的创建过程,比较函数的创建的源码:


private static EqualityComparer<T> CreateComparer()
{
    Contract.Ensures(Contract.Result<EqualityComparer<T>>() != null);

    RuntimeType t = (RuntimeType)typeof(T);
    // Specialize type byte for performance reasons
    if (t == typeof(byte)) {
        return (EqualityComparer<T>)(object)(new ByteEqualityComparer());
    }
    // If T implements IEquatable<T> return a GenericEqualityComparer<T>
    if (typeof(IEquatable<T>).IsAssignableFrom(t)) {
        return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<int>), t);
    }
    // If T is a Nullable<U> where U implements IEquatable<U> return a NullableEqualityComparer<U>
    if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) {
        RuntimeType u = (RuntimeType)t.GetGenericArguments()[0];
        if (typeof(IEquatable<>).MakeGenericType(u).IsAssignableFrom(u)) {
            return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(NullableEqualityComparer<int>), u);
        }
    }
    
    // See the METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST and METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST_LONG cases in getILIntrinsicImplementation
    if (t.IsEnum) {
        TypeCode underlyingTypeCode = Type.GetTypeCode(Enum.GetUnderlyingType(t));

        // Depending on the enum type, we need to special case the comparers so that we avoid boxing
        // Note: We have different comparers for Short and SByte because for those types we need to make sure we call GetHashCode on the actual underlying type as the 
        // implementation of GetHashCode is more complex than for the other types.
        switch (underlyingTypeCode) {
            case TypeCode.Int16: // short
                return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(ShortEnumEqualityComparer<short>), t);
            case TypeCode.SByte:
                return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(SByteEnumEqualityComparer<sbyte>), t);
            case TypeCode.Int32:
            case TypeCode.UInt32:
            case TypeCode.Byte:
            case TypeCode.UInt16: //ushort
                return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(EnumEqualityComparer<int>), t);
            case TypeCode.Int64:
            case TypeCode.UInt64:
                return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(LongEnumEqualityComparer<long>), t);
        }
    }
    // Otherwise return an ObjectEqualityComparer<T>
    return new ObjectEqualityComparer<T>();
}

我们看到源码中,对数字,byte,有‘比较’接口(IEquatable<T>),和没有‘比较’接口,四种方式进行了区分对待。

对于像数字和byte类的,比较容易比较,所以它们都是一类,且是有相应固定的比较函数的。而有‘比较’接口(IEquatable<T>)的实体,则直接使用GenericEqualityComparer<T>来获得哈希函数。最后那些没有‘比较’接口(IEquatable)的实体,如果继承了 Nullable\<U\> 接口,则使用一个叫 NullableEqualityComparer 的比较函数来代替。如果什么都不是,就只能使用 ObjectEqualityComparer\<T\> 默认的对象比较方式来做比较了。

在C#里所有类都继承了 Object 类,所以即使没有特别的重写 Equals 函数,都会使用 Object 类的 Equals 函数:


public virtual bool Equals(Object obj)
{
    return RuntimeHelpers.Equals(this, obj);
}

[System.Security.SecuritySafeCritical]  // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public new static extern bool Equals(Object o1, Object o2);

而这个 Equals 两个对象的比较,是以内存地址为基准的。

Dictionary 同List一样并不是线程安全的组件,官方源码中进行了这样的解释。
    ** Hashtable has multiple reader/single writer (MR/SW) thread safety built into 
    ** certain methods and properties, whereas Dictionary doesn't. If you're 
    ** converting framework code that formerly used Hashtable to Dictionary, it's
    ** important to consider whether callers may have taken a dependence on MR/SW
    ** thread safety. If a reader writer lock is available, then that may be used
    ** with a Dictionary to get the same thread safety guarantee. 

Hashtable在多线程读写中是线程安全的,而 Dictionary 不是。如果要在多个线程中共享Dictionaray的读写操作,就要自己写lock以保证线程安全。

到这里我们已经全面了解了 Dictionary 的内部构造和运作机制。他是由数组构成,并且由哈希函数完成地址构建,由拉链法冲突解决方式来解决冲突。

从效率上看,同List一样最好在 实例化对象时,即 new 时尽量确定大致数量会更加高效,另外用数值方式做Key比用类实例方式作为Key值更加高效率。

从内存操作上看,大小以3->7->17->37->….的速度,每次增加2倍多的顺序进行,删除时,并不缩减内存。

如果想在多线程中,共享 Dictionary 则需要进行我们自己进行lock操作。

Dictionary源码

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【python】如何用python写一个下拉选择框和页签?

文章目录前言ttk模块下拉选择框combobox下拉选择框2页签Notebook前言 python学习之路任重而道远&#xff0c;要想学完说容易也容易&#xff0c;说难也难。 很多人说python最好学了&#xff0c;但扪心自问&#xff0c;你会用python做什么了&#xff1f; 刚开始在大学学习c语言&…

【玩转c++】stack和queue的介绍和模拟实现

本期主题&#xff1a;list的讲解和模拟实现博客主页&#xff1a; 小峰同学分享小编的在Linux中学习到的知识和遇到的问题小编的能力有限&#xff0c;出现错误希望大家不吝赐stack的介绍和使用1.1.stack的介绍1. stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上…

论文阅读-MGTAB: A Multi-Relational Graph-Based Twitter Account DetectionBenchmark

目录 摘要 1. 引言 2. 相关工作 2.1. 立场检测 2.2.机器人检测 3.数据集预处理 3.1.数据收集和清理 3.2.专家注释 3.3. 质量评估 3.4.特征分析 4. 数据集构建 4.1.特征表示构造 4.2.关系图构建 5. 实验 5.1.实验设置 5.2.基准性能 5.3训练集大小的研究 5.4 社…

Matlab进阶绘图第6期—雷达图/蜘蛛图/星图

雷达图&#xff08;Radar Chart&#xff09;&#xff0c;又称星图、蜘蛛图、蜘蛛网图、网络图、Kiviat图等&#xff0c;是一种以从同一点开始的轴上表示的三个以上变量的二维图表的形式&#xff0c;来显示多变量数据的图形方法。 雷达图可以直观地对多维数据集目标对象的性能、…

三步搞定OOM内存溢出,记一次使用Arthas处理OOM内存溢出问题java.lang.OutOfMemoryError: Java heap space

记一次OOM内存溢出问题修复java.lang.OutOfMemoryError: Java heap spaceOutOfMemoryError1.使用article找到问题线程2.分析线程运行链路&#xff0c;找出问题代码位置3.使用堆文件确认问题Arthas 是Alibaba开源的Java诊断工具&#xff0c;功能强大&#xff0c;操作简单 Arthas…

我们为什么使用docker 优点 作用

1. 我们为什么使用Docker? 当我们在工作中&#xff0c;一款产品从开发设计到上线运行&#xff0c;其中需要开发人员和运维工程师&#xff0c;开发人员负责代码编写&#xff0c;开发产品&#xff0c;运维工程师需要测试环境&#xff0c;产品部署。这之间就会有分歧。 就好比我…

信创国产化,试试 Solon v2.2.2

Solon 是一个高效的 Java 应用开发框架&#xff1a;更快、更小、更简单。它不是 Spring、没有用 Servlet、也无关 JavaEE&#xff0c;是一个有自己接口标准的开放生态。可以为应用软件国产化提供支持&#xff0c;助力信创建设。 150来个生态插件&#xff0c;覆盖各种不同的应用…

不知道Redis?来这里可以带你快速学完Redis,干活满满!

文章目录一、NoSQL的基本介绍二、为什么要使用NoSQL&#xff0c;难道SQL不够你用吗&#xff1f;三、Redis的基本概念四、Redis基本操作命令五、Redis五大数据类型及其操作命令六、三种特殊的数据类型及其操作命令七、 Redis事务八、Redis对key的监控九、Redis数据库密码十、Jed…

广州蓝景分享—8大Web前端开发的趋势

2023 年 1 月 11 日&#xff0c;2022 年度 StateOfJS 调查结果正式公布&#xff01;StateOfJS 是前端生态圈中比较有影响力的且规模较大的数据调查。本文就来解读一下 2022 年 StateOfJS 的调查结果&#xff01; JavaScript 发展很快&#xff0c;但似乎 JavaScript 开发人员的…

《传感器技术》考试学习笔记

文章目录一、选择题二、简答题1.什么是传感器&#xff1f;传感器的共性是哪些&#xff1f;2.差动变气隙式传感器电感传感器的灵敏度推导过程是什么&#xff08;推导公式&#xff09;&#xff1f;与单极性进行比较它们的优缺点是哪些&#xff1f;3.霍尔传感器如何进行微位移测量…

uniapp上实现左右关联滚动

先看效果&#xff1a; 代码&#xff1a; <template><view class"container"><!-- 左侧fixed导航区域 --><view class"left"><viewv-for"item in leftList":key"item.id"class"left_item":class…

JVM结构-类加载(类加载子系统,类加载的角色,类加载的过程,类加载器分类,双亲委派机制,类的主/被动使用)

JVM 结构-类加载2.1类加载子系统2.2类加载的角色2.3类加载的过程2.3.1加载2.3.2链接2.3.3初始化2.4类加载器分类2.4.1 引导类加载器2.4.2扩展类加载器2.4.3应用程序类加载器2.5双亲委派机制2.6类的主动/被动使用2.1类加载子系统 类加载器子系统负责从文件系统或者网络中加载 cl…

【深度强化学习】(1) DQN 模型解析,附Pytorch完整代码

大家好&#xff0c;今天和各位讲解一下深度强化学习中的基础模型 DQN&#xff0c;配合 OpenAI 的 gym 环境&#xff0c;训练模型完成一个小游戏&#xff0c;完整代码可以从我的 GitHub 中获得&#xff1a; https://github.com/LiSir-HIT/Reinforcement-Learning/tree/main/Mod…

腾讯云新用户怎么配置服务器的方法教程

腾讯云新用户怎么配置服务器&#xff1f;腾讯云服务器配置选择攻略&#xff0c;先选择云服务器地域和可用区&#xff0c;然后根据用户使用场景需要平衡型、计算型或高IO型等特性来选择云服务器CVM实例规格&#xff0c;主机教程网来详细说下腾讯云服务器配置选择攻略。 1、腾讯云…

政府工作报告连提9年科技创新 企业研发如何“又快又好”

今年的政府工作报告&#xff0c; “科技创新” 这一描述连续出现7次&#xff0c;这也是自2015年开始&#xff0c; “科技创新” 这一概念在全国“两会”政府工作报告中连续九年被提到。政府工作报告指出&#xff0c;科技政策要聚焦自立自强&#xff0c;完善新型举国体制&#x…

ccc-pytorch-LSTM(8)

文章目录一、LSTM简介二、LSTM中的核心结构三、如何解决RNN中的梯度消失/爆炸问题四、情感分类实战&#xff08;google colab&#xff09;一、LSTM简介 LSTM&#xff08;long short-term memory&#xff09;长短期记忆网络&#xff0c;RNN的改进&#xff0c;克服了RNN中“记忆…

ArcGIS动态表格批量出图

一.产品介绍&#xff1a;ArcGIS动态表格扩展模块Mapping and Charting Solutions&#xff0c;可用于插入动态表格&#xff0c;与数据驱动结合&#xff0c;出图效率无敌。注&#xff1a;优先选择arcgis10.2.2。 二、下载连接&#xff1a; https://www.xsoftnet.com/share/a001CX…

Idea+maven+spring-cloud项目搭建系列--12 整合grpc

前言&#xff1a; grpc 是geogle 开源的rpc 通信框架&#xff0c;通过定义proto生成通信存根&#xff0c;像本地调用服务一样&#xff0c;进行远程服务的调用&#xff1b; 1 消费端服务提供&#xff1a; 1.1 引入grpc 和 protobuf <!-- RPC --> <!-- RPC 服务调用 …

最大子矩阵(蓝桥杯)暴搜 JAVA

题目描述&#xff1a; 小明有一个大小为NM的矩阵&#xff0c;可以理解为一个N行M列的二维数组。 我们定义一个矩阵m 的稳定度f(m) 为f(m)max(m)-min(m)。 其中max(m)表示矩阵m中的最大值&#xff0c;min(m) 表示矩阵m 中的最小值。 现在小明想要从这个矩阵中找到一个稳定度不大…

2023-03-09干活小计

强化学习&#xff1a; 强化学习用智能体&#xff08;agent&#xff09;这个概念来表示做决策的机器。 感知、决策和奖励 感知。智能体在…