zoukankan      html  css  js  c++  java
  • 浅谈C#中Dictionary的实现。

    引言

    Dictionary在C#中算是比较常用的数据结构了,它的读取速度很快,效率很高。以前没有怎么看它的源码实现,前几天看了看它的源码实现,还是有点意思的,下面我将逐步说下它的实现原理。

    数据结构

    它是通过Hash Bucket和链表形成的数据结构,将一份数据分为多个链表,且每个链表都对应它的Bucket。可以看以下的图:

    看不明白不要急,我们先看源码Dictionary类里面定义的字段都有什么。

    private struct Entry {
        public int hashCode;    // 每个K/V对应的Hash值
        public int next;        // 指向下一个K/V的index,-1代表最后一个
        public TKey key;           // key的值
        public TValue value;         // Value的值
    }
    
    private int[] buckets; //定义桶的数组
    private Entry[] entries; //定义元素的数组
    private int count; //元素总数量
    private int version; //版本
    private int freeList; //被移除后,空闲元素的下标
    private int freeCount; //空闲元素的数量
    

    有了上面字段的定义,接下来我分分析它是怎么添加元素,寻找元素和删除元素的。

    添加元素

    首先我们先把主要的源码贴出来,从源码上分析主要实现。

    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; //获取对应的目标Bucket
    
        //寻找是否有相同key的元素
        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;
            } 
    
        }
        int index;
        if (freeCount > 0) { //判断现在是否有空闲的元素,优先使用空闲的元素
            index = freeList;
            freeList = entries[index].next;
            freeCount--;
        }
        else {  
            if (count == entries.Length) //判断是否存储的项和Entries的长度,相等的话,就重新扩容。
            {
                Resize();// 扩容Buctet和Entries的大小
                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++;
    }
    

    源码详细讲解。

    1. 初次添加元素时,如果构造函数中不传入大小,默认会自动取一个最小的质数(即:3)来作为桶的大小和元素集合的大小,并且初始换里面的字段变量。
    2. 在寻找元素插入的位置的时候,首先通过元素的hashcode % bucket的长度优先得到要插入的目标的Bucket是哪个,然后将元素的hashCode和next赋值。
    3. 这里next赋值的话详细说一下。首先,我们先分析buckets,它里面的每个bucket保存的是对应链表最后一个元素的下标,可以通过最后一行代码得知,每次给元素赋值之后,当前元素的下标,会赋值给对应的bucket。
      而每个新插入元素,只需要当前bucket里面的值赋值给它当前的next指向的index就可以了。

    图示

    查找元素

    我们还是先简单看一下主要实现部分的源码。

    private int FindEntry(TKey key) {
        if( key == null) {
            ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
        }
    
        if (buckets != null) {
            int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; //首先获取key的hashCode
            //寻址的第一个元素就是对应目标桶里面记录的index,然后通过对应元素的next指向下一个元素,当next为-1时,就是代表已经到最后一个元素了。
            for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {
                //判断元素的hashcode和key是否都相等。
                if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
            }
        }
        return -1;
    }
    

    源码详细讲解。

    1. 其实这里代码已经写的很清楚了,它是先找到目标桶,因为目标桶里面记录的是它对应链表的最后一个元素的下标,然后顺着元素的next找,直到找到这个元素为止。
    2. 可以仔细想想,这样的话,每次查找就可以过滤一大批的数据,所以查的速度就更快了,但是当数据量大的时候,也是会有效率问题。

    图示

    移除元素

    还是先简单看下源代码主要实现的部分。

    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; //把buctet指向的下标,指向下一个元素
                    }
                    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++; //每次移除,空闲的元素+1
                    version++;
                    return true;
                }
            }
        }
        return false;
    }
    

    源码详细讲解。

    1. 查找对应的元素和上面的逻辑其实是一样的,它这里定义了一个变量,用来记录上一个元素的下标。
    2. 当找到对应的元素时,把上个元素的next指向当前被移除元素的next,即把当前被移除的元素跳过去。
    3. 然后将被移除元素的字段初始化,需要注意的是这个next的值,它用的是freelist,记录最后一个被移除元素的index,每移除一个元素,被移除的元素数量就+1,即freecount。
      被移除的元素也会形成一个链表,它的next首部元素next指向-1,后边被移除的元素next指向上一个被移除元素的index。
    4. 回过头再去看添加的时候,它会判断,freeCount的数量是否是大于0的,如果大于0的话,优先使用被移除元素的位置并填充它们,它的index就是freeList,然后再把当前元素的next赋值给
      freelist(即下次再插入元素的时候,就是上一个被移除元素的下标)。
    5. 最后在下面给当前元素赋值的时候,它的next又指向当前bucket里面的值,即作为对应链表的尾部。

    图示

    关于扩容

    先简单看下主要实现部分的源码。

    private void Resize(int newSize, bool forceNewHashCodes) {
        Contract.Assert(newSize >= entries.Length); // 这个newSize是获取大于count的最小质数
        int[] newBuckets = new int[newSize];
        for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1; //初始化每个bucket的值
        Entry[] newEntries = new Entry[newSize];
        Array.Copy(entries, 0, newEntries, 0, count); //将原来的entries的值copy到新的entries里面
    
        for (int i = 0; i < count; i++) {
            if (newEntries[i].hashCode >= 0) { //判断hashcode代表是有效的entry
                int bucket = newEntries[i].hashCode % newSize; 
                newEntries[i].next = newBuckets[bucket];
                newBuckets[bucket] = i; //上面的操作就是重新找新的桶,然后重新给entey的next赋值。
            }
        }
        buckets = newBuckets;
        entries = newEntries;
    }
    

    源码详细讲解

    1. 这里的newSize会在里面的元素数达到entries的长度是扩容,它是在一个helper类里面进行取值,它是拿大于它的最小质数。
    2. 这个新的size就是buckets的长度和entries的长度,先把原来的entries的值copy到新的entries里面。
    3. 然后循环新的的entries,重新定义新的桶的值并且给原来的数据,重新形成新的链表。

    总结

    关于dictionary的解析也是我一个简单的理解吧,主要还是看源码能了解的更多,希望能够帮助到需要的人。
    微软源码的地址可以看这个: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,bba8e22685381014

  • 相关阅读:
    算法----(1)冒泡排序
    淘宝爬虫
    爬虫_豆瓣电影top250 (正则表达式)
    爬虫_猫眼电影top100(正则表达式)
    Android 简单调用摄像头
    Android 简单天气预报
    思维模型
    This view is not constrained, it only has designtime positions, so it will jump to (0,0) unless you
    Android studio preview界面无法预览,报错render problem
    Android studio 3.1.2报错,no target device found
  • 原文地址:https://www.cnblogs.com/snailZz/p/13612953.html
Copyright © 2011-2022 走看看