c# dictionary 深度剖析

主要思想:http://www.cnblogs.com/wangjun1234/p/3719635.html

代码剖析;http://blog.csdn.net/exiaojiu/article/details/51252515

 

笔记:

hash算法:除留余数法 F(key)%m

内部有两个数组,一个bucket,一个装数据的entries, 一个entry有key,value,hashcode,next(有冲突时放下一个bucket地址)

插入:将数据放入空闲列表第一个位置,假如为entries[i],并将空闲列表的head置为entries[i].next, 计算hash,如果为2,如果没冲突,在bucket[2]里面填I,如果有冲突,计算新的hashcode,假设为N,将bucket[2]对应的entry的next设置为N。

查找:计算hash,假设为N,在bucket[N] 处找到entry的索引,比较key是不是相等,如果不相等,说明存在冲突,取出next,比较下一个的key,直到比较成功或者next为-1(比较失败)

删除:查找key对应的entry,将entry.next 放到当前bucket位置,当前entry放到空闲链表里。

与hashtable的比较:dictionary查找需两次,但插入快。

rsize的时候,dictionary把数据全部搬到新的entry上,必要时在重新计算hash


这里写图片描述

 

 

从内部剖析C# 集合之--Dictionary

 Dictionary和hashtable用法有点相似,他们都是基于键值对的数据集合,但实际上他们内部的实现原理有很大的差异,

先简要概述一下他们主要的区别,稍后在分析Dictionary内部实现的大概原理。

区别:1,Dictionary支持泛型,而Hashtable不支持。

        2,Dictionary没有装填因子(Load Facto)概念,当容量不够时才扩容(扩容跟Hashtable一样,也是两倍于当前容量最小素数),Hashtable是“已装载元素”与”bucket数组长度“大于装载因子时扩容。

        3,Dictionary内部的存储value的数组按先后插入的顺序排序,Hashtable不是。

       4,当不发生碰撞时,查找Dictionary需要进行两次索引定位,Hashtable需一次,。

 Dictionary采用除法散列法来计算存储地址,想详细了解的可以百度一下,简单来说就是其内部有两个数组:buckets数组和entries数组(entries是一个Entry结构数组),entries有一个next用来模拟链表,该字段存储一个int值,指向下一个存储地址(实际就是bukets数组的索引),当没有发生碰撞时,该字段为-1,发生了碰撞则存储一个int值,该值指向bukets数组.

c# dictionary 深度剖析_第1张图片c# dictionary 深度剖析_第2张图片

 

下面跟上次一样,按正常使用Dictionary时,看内部是如何实现的。

一,实例化一个Dictionary, Dictionary dic=new Dictionary();

    a,调用Dictionary默认无参构造函数。

    b,初始化Dictionary内部数组容器:buckets int[]和entries[],分别分配长度3。(内部有一个素数数组:3,7,11,17....如图:);

  二,向dic添加一个值,dic.add("a","abc");

     a,将bucket数组和entries数组扩容3个长度。

     b,计算"a"的哈希值,

     c,然后与bucket数组长度(3)进行取模计算,假如结果为:2

     d,因为a是第一次写入,则自动将a的值赋值到entriys[0]的key,同理将"abc"赋值给entriys[0].value,将上面b步骤的哈希值赋值给entriys[0].hashCode,

       entriys[0].next 赋值为-1,hashCode赋值b步骤计算出来的哈希值。

    e,在bucket[2]存储0。

三,通过key获取对应的value,  var v=dic["a"];

   a, 先计算"a"的哈希值,假如结果为2,

   b,根据上一步骤结果,找到buckets数组索引为2上的值,假如该值为0.

   c, 找到到entriys数组上索引为0的key,

         1),如果该key值和输入的的“a”字符相同,则对应的value值就是需要查找的值。

         2) ,如果该key值和输入的"a"字符不相同,说明发生了碰撞,这时获取对应的next值,根据next值定位buckets数组(buckets[next]),然后获取对应buckets上存储的值在定位到entriys数组上,......,一直到找到为止。

         3),如果该key值和输入的"a"字符不相同并且对应的next值为-1,则说明Dictionary不包含字符“a”。

 

Dictionary里的其他方法就不说了,各位可以自己去看源码,下面来通过实验来对比Hashtable和Dictionary的添加和查找性能,

1,添加元素速度测评。

     循环5次,每次内部在循环10次取平均值,PS:代码中如有不公平的地方望各位指出,本人知错就改。

 a,值类型

 View Code

 结果:

c# dictionary 深度剖析_第3张图片

通过运行结果来看,HashTable 速度明显慢于Dictionary,相差一个数量级。我个人分析原因可能为:

   a,Hashtable不支持泛型,我向你添加的int类型会发生装箱操作,而Dictionary支持泛型。

   b,Hashtable在扩容时会先new一个更大的数组,然后将原来的数据复制到新的数组里,还需对新数组里的key重新哈希计算(这可能是最性能影响最大的因素)。而Dictionary不会这样。

b,引用类型

 View Code

c# dictionary 深度剖析_第4张图片

Dic速度还是比Hashtable快,但没有值类型那么明显,这个测试可能有不准的地方。

2,查找速度测评(两种情况:值类型和引用类型)

1 值类型

 View Code

运行结果

2,引用类型

 

 View Code

运行结果

 

 根据上面实验结果可以得出:

 a,值类型,Hashtable和Dictionary性能相差不大,Hashtable稍微快于Dictionary.

 b,引用类型:Hashtable速度明显快于Dictionary。

 

源代码版本为 .NET Framework 4.6.1

本系列持续更新,敬请关注

有投入,有产出。

(注:非基础性,主要涉及Dictionary的实现原理)

水平有限,若有不对之处,望指正。

Dictionary是Hashtable的一种泛型实现(也是一种哈希表)实现了IDictionary反应接口和非泛型接口等,将键映射到相应的值。任何非 null 对象都可以用作键。使用与Hashtable不同的冲突解决方法,Dictionary使用拉链法。

概念重播

对于不同的关键字可能得到同一哈希地址,即key1 != key2 => F(key1)=F(fey2),这种现象叫做冲突,在一般情况下,冲突只能尽可能的少,而不能完全避免。因为,哈希函数是从关键字集合到地址集合的映像。通常,关键字集合比较大,它的元素包括多有可能的关键字。既然如此,那么,如何处理冲突则是构造哈希表不可缺少的一个方面。

通常用于处理冲突的方法有:开放定址法、再哈希法、链地址法、建立一个公共溢出区等。

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

哈希函数

Dictionary使用的哈希函数是除留余数法,在源码中的公式为:

h = F(k) % m; m 为哈希表长度(这个长度一般为素数)

通过给定或默认的GetHashCode()函数计算出关键字的哈希码模以哈希表长度,计算出哈希地址。

拉链法

Dictionary使用的解决冲突方法是拉链法,又称链地址法。

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

结构图

基本成员

        private struct Entry {
            public int hashCode;    //31位散列值,32最高位表示符号位,-1表示未使用
            public int next;        //下一项的索引值,-1表示结尾
            public TKey key;        //键
            public TValue value;    //值
        }

        private int[] buckets;//内部维护的数据地址
        private Entry[] entries;//元素数组,用于维护哈希表中的数据
        private int count;//元素数量
        private int version;
        private int freeList;//空闲的列表
        private int freeCount;//空闲列表元素数量
        private IEqualityComparer comparer;//哈希表中的比较函数
        private KeyCollection keys;//键集合
        private ValueCollection values;//值集合
        private Object _syncRoot;

 

buckets 就想在哈希函数与entries之间解耦的一层关系,哈希函数的F(k)变化不在直接影响到entries。 
freeList 类似一个单链表,用于存储被释放出来的空间即空链表,一般有被优先存入数据。 
freeCount 空链表的空位数量。

初始化函数 
该函数用于,初始化的数据构造

        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;
        }

size 哈希表的长度是素数,可以使元素更均匀地分布在每个节点上。 
buckets 中的节点值,-1表示空值。 
freeList 为-1表示没有空链表。 
buckets 和 freeList 所值指向的数据其实全是存储于一块连续的内存空间(entries )之中。

插入元素

        public void Add(TKey key, TValue value) {
            Insert(key, value, true);
        }
        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;

            //循环冲突
            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;
                } 

                collisionCount++;
            }

            //添加元素
            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;

            //单链接的节点数(冲突数)达到了一定的阈值,之后更新散列值
            if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
            {
                comparer = (IEqualityComparer) HashHelpers.GetRandomizedEqualityComparer(comparer);
                Resize(entries.Length, true);
            }
        }

思路分析: 
(1)通过哈希函数寻址,计算出哈希地址(因为中间有一个解耦关系buckets,所以不再直接指向entries的索引值,而是buckets的索引)。 
(2)判断buckets中映射到的值是否为-1(即为空位)。若不为-1,表示有冲突,遍历冲突链,不允许重复的键。 
(3)判断是否有空链表,有则插入空链表的当前位置,将freeList指针后移,freeCount减一,否则将元素插入当前空位。在这一步,容量不足将自动扩容,若当前位置已经存在元素则将该元素的地址存在插入元素的next中,形成一个单链表的形式。类似下图中的索引0。 
这里写图片描述

移除

        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;//移除的元素散列值值为-1
                        entries[i].next = freeList;//将移除的元素放入空列表
                        entries[i].key = default(TKey);
                        entries[i].value = default(TValue);
                        freeList = i;//记录当前地址,以便下个元素能直接插入
                        freeCount++;//空链表节点数+1
                        version++;
                        return true;
                    }
                }
            }
            return false;
        }

Dictionary中存储元素的结构非常有趣,通过一个数据桶buckets将哈希函数与数据数组进行了解耦,使得每一个buckets的值对应的都是一条单链表,在内存空间上却是连续的存储块。同时Dictionary在空间与性能之间做了一些取舍,消耗了空间,提升了性能(影响性能的最大因素是哈希函数)。 
移除思路分析: 
(1)通过哈希函数确定单链表的位置,然后进行遍历。 
(2)该索引对应的值为-1,表示没有没有单链接节点,返回false,结束 
(3)该索引对应的值大于-1,表示有单链表节点,进行遍历,对比散列值与key,将映射到的entries节点散列值赋-1,next指向空链表的第一个元素地址(-1为头节点),freeList指向头节点地址,空链表节点数+1,返回true,结束;否则返回false,结束。(此处的节点地址统指索引值)。

查询

        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;

 

        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;
        }

代码是不是一目了然,在FindEntry方法中,定位单链接的位置,进行遍历,对比散列值与key,比较成功则返回true,结束。

扩容

        private void Resize() {
            Resize(HashHelpers.ExpandPrime(count), false);
        }

        private void Resize(int newSize, bool forceNewHashCodes) {
            Contract.Assert(newSize >= entries.Length);

            //重新初始化一个比原来空间还要大2倍左右的buckets和Entries,用于接收原来的buckets和Entries的数据
            int[] newBuckets = new int[newSize];
            for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;
            Entry[] newEntries = new Entry[newSize];

            //数据搬家
            Array.Copy(entries, 0, newEntries, 0, count);

            //将散列值刷新,这是在某一个单链表节点数到达一个阈值(100)时触发
            if(forceNewHashCodes) {
                for (int i = 0; i < count; i++) {
                    if(newEntries[i].hashCode != -1) {
                        newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
                    }
                }
            }

            //单链表数据对齐,无关顺序
            for (int i = 0; i < count; i++) {
                if (newEntries[i].hashCode >= 0) {
                    int bucket = newEntries[i].hashCode % newSize;
                    newEntries[i].next = newBuckets[bucket];
                    newBuckets[bucket] = i;
                }
            }
            buckets = newBuckets;
            entries = newEntries;
        }

foreach遍历 
Dictionary实现了IEnumerator接口,是可以用foreach进行遍历的,遍历的集合元素类型为KeyValuePair,是一种键值对的结构,实现是很简单的,包含了最基本的键属性和值属性, 
从代码中可以看出,用foreach遍历Dictionary就像用for遍历一个基础数组一样。 
这是内部类Enumerator(遍历就是对它进行的操作)中的方法MoveNext(实现IEnumerator接口的MoveNext方法)。


            public bool MoveNext() {
                if (version != dictionary.version) {
                    ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
                }

                while ((uint)index < (uint)dictionary.count) {
                    if (dictionary.entries[index].hashCode >= 0) {
                        current = new KeyValuePair(dictionary.entries[index].key, dictionary.entries[index].value);
                        index++;
                        return true;
                    }
                    index++;
                }

                index = dictionary.count + 1;
                current = new KeyValuePair();
                return false;
            }
    public struct KeyValuePair {
        private TKey key;
        private TValue value;

        public KeyValuePair(TKey key, TValue value) {
            this.key = key;
            this.value = value;
        }

        //键属性
        public TKey Key {
            get { return key; }
        }

        //值属性
        public TValue Value {
            get { return value; }
        }

        public override string ToString() {
            StringBuilder s = StringBuilderCache.Acquire();
            s.Append('[');
            if( Key != null) {
                s.Append(Key.ToString());
            }
            s.Append(", ");
            if( Value != null) {
               s.Append(Value.ToString());
            }
            s.Append(']');
            return StringBuilderCache.GetStringAndRelease(s);
        }
    }

 

最后 
Dictionary内部实现结构比Hashtable复杂,因为具有单链表的特性,效率也比Hashtable高。

 

 

 

你可能感兴趣的:(面试准备,C#)