热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

[c#基础知识]浅谈Hashtable与Dictionary的异同

以前对于这两个集合类的认识只是停留在是否支持泛型上,这几天趁着看算法导论的机会,把两个类的内部的实现机制好好的了解了一下。Hashtable和Dictionary从数据结构上来说都属于Hashtabl

以前对于这两个集合类的认识只是停留在是否支持泛型上,这几天趁着看算法导论的机会,把两个类的内部的实现机制好好的了解了一下。

Hashtable和Dictionary从数据结构上来说都属于Hashtable,都是对关键字(键值)进行散列操作,将关键字散列到Hashtable的某一个槽位中去,不同的是处理碰撞的方法。散列函数有可能将不同的关键字散列到Hashtable中的同一个槽中去,这个时候我们称发生了碰撞,为了将数据插入进去,我们需要另外的方法来解决这个问题。

链接法(chaining)

在链接法中,把散列到同一个槽中的所有元素放在一个链表中,槽中有一个指针,指向链表的头,如果没有的话,则为NIL。对于一个能存放n个元素,具有m个槽位的散列表,我们定义装载因子a为n/m,即一个链中平均存储的元素的个数。

链接法中的加入,删除,寻找操作其实基本上就是链表的基本操作。在这里就不仔细讲了。

image 

开放寻址法(open addressing)

在开放寻址法中,所有的元素都保存在散列表中,而不是像链接法,数据保存在外部的链表中,在开放寻址法中,由于数据全部存储在散列表中,所以槽位一定会大于等于n,也就是说,装载因子一定会小于等于1。

在开放寻址法中,当要插入一个元素时,我们将关键字和探查号(从0开始累加)作为输入传给散列函数,散列函数返回对应的槽位。插入的时候首先查找hash(key,0)这个槽,如果不为空则探查号+1,继续查下一个槽,直到找到空槽,或者得知散列表已满。查找的过程和插入类似,查找关键字的时候如果我们碰到了空槽,查找就结束,因为如果关键字存在的话,那么也应该会出现在这个地方。

开放寻址法中比较特殊的是删除操作,如果删除数据置为null的话,那么就会有一个问题,比如我们插入过程中插入k的时候发现槽i已经被占用,我们插到后面的槽中,如果删除的时候我们简单的将槽i置为null,那么查找的时候关键字k就不会被找到。这个问题我们可以用一个标志位来解决。具体的实现会在下面讲到。

双重散列

开放寻址法的探查方法有多种,在这里只讲一下双重探查,因为这种方法是最好的方法之一,而且它被用在Hashtable中。

这里为辅助散列函数,第一次为,后续的探查位置在的基础上加上偏移量,然后对m进行模运算。这里需要提一下的是为了查找整个散列表,需要与槽的大小m互质,等下可以看到在Hashtable类中是如何满足这个条件的。

image

在解释了链接法和开放寻址法后,来讲讲Hashtable和Dictionary。

Hashtable这个类采用的是开放寻址法来解决碰撞的问题,下面来看看Hashtable的一个构造函数

  
  1. this.loadFactor = 0.72f * loadFactor;    
  2.  double num = ((float) capacity) / this.loadFactor;    
  3.  if (num > 2147483647.0)    
  4.  {    
  5.    throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));    
  6. }    
  7.  int num2 = (num > 11.0) ? HashHelpers.GetPrime((int) num) : 11;    
  8.  this.buckets = new bucket[num2];    
  9.  this.loadsize = (int) (this.loadFactor * num2);    
  10.  this.isWriterInProgress = false;  
 

构造函数会在传入装载因子的基础上乘以0.72,这个值是微软认为的比较理想的一个值。上面已经说过了在双重散列时需要保持和槽的大小m互质,我们只需要保证m为质数,而比m小,这样就能保证他们总是互质。在这里HashHelpers.GetPrime实现的就是传回一个比num大的质数,这样能保证num2这个量总为一个质数,然后把槽数组建立起来。

(this.GetHash(key) & 0x7fffffff)这个相当于双散列公式中的,1 + ((uint) (((seed >> 5) + 1) % (hashsize - 1)));则相当于,

槽中的hash_coll用来存放key对应的hashcode,最高位用来标识是否发生了碰撞,发生碰撞的槽的最高位会被置为1,搜索的时候,如果最高位为1那么搜寻函数会继续搜索,注意contains方法中的while条件,

   
  1. do   
  2.  {    
  3.     bucket = buckets[index];    
  4.    if (bucket.key == null)    
  5.    {    
  6.       return false;    
  7.     }    
  8.     if (((bucket.hash_coll & 0x7fffffff) == num3) && this.KeyEquals(bucket.key, key))    
  9.     {    
  10.        return true;    
  11.    }    
  12.     index = (int) ((index + num2) % ((ulong) buckets.Length));    
  13.  }    
  14.  while ((bucket.hash_coll < 0) && (++num4 < buckets.Length));  

BTW,我当时看这个方法的时候觉得搜寻函数其实也可以通过跳过bucket.key == this.buckets的项来写,因为在移除方法中如果bucket.hash_coll <0的话,那么bucket.key = this.buckets, 后来想了一下,bucket.hash_coll <0这样效率更高,这里就不说为什么了,爱思考的朋友在后面写下你的答案吧。

在Add方法里面需要对count进行检查,如果达到了设定的值,这个时候需要对Hashtable进行扩容,扩大的容量是当前容量的2倍以上的一个质数,然后对里面已经存在的元素重新进行hash操作,相当于重新插入新的槽数组中。对于Insert方法中的index这个变量的作用我在看代码的时候还是有点疑问的,如果有知道的朋友麻烦在留言中告知。

Dictionary这个泛型类采用的是链接法来解决碰撞,其中的bucket存储的是指向Entry的下标,Entry就相当于链表中的节点,Entry中存储的又有指向下一个产生碰撞的元素的下标。稍有不同的是,这里的Entry是一个数组。

   
  1. public struct Entry    
  2. {    
  3.    public int hashCode;    
  4.    public int next;    
  5.   public TKey key;    
  6.    public TValue value;    
  7.  }  

Dictionary的Add操作首先计算元素的Hash值,然后根据Hash值寻找bucket,找到相应的bucket后将值存入Entry中,并将bucket指向相应的Entry.查询操作逻辑是根据Hash值找到相应的bucket然后通过bucket到Entry数组中进行寻找。

稍微需要提一下的是Remove方法,为了将删除的节点的Entry进行重用,Dictionary中有一个freeList字段,删除的节点的下标值,为赋给freeList,在Add操作的时候如果freeList>0则将数据插入到freeList指向的Entry中去。

原文链接:http://www.cnblogs.com/MichaelYin/archive/2011/02/14/1954724.html


推荐阅读
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • hashmap线程不安全允许有null的键和值效率高一点、方法不是Synchronize的要提供外同步有containsvalue和containsKey方法HashMap是Java1 ... [详细]
  • 要讨论这些常用的默认初始容量和扩容的原因是:当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全部复 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了一种轻巧方便的工具——集算器,通过使用集算器可以将文本日志变成结构化数据,然后可以使用SQL式查询。集算器利用集算语言的优点,将日志内容结构化为数据表结构,SPL支持直接对结构化的文件进行SQL查询,不再需要安装配置第三方数据库软件。本文还详细介绍了具体的实施过程。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • Java编程实现邻接矩阵表示稠密图的方法及实现类介绍
    本文介绍了Java编程如何实现邻接矩阵表示稠密图的方法,通过一个名为AMWGraph.java的类来构造邻接矩阵表示的图,并提供了插入结点、插入边、获取邻接结点等功能。通过使用二维数组来表示结点之间的关系,并通过元素的值来表示权值的大小,实现了稠密图的表示和操作。对于对稠密图的表示和操作感兴趣的读者可以参考本文。 ... [详细]
  • 本文介绍了在实现了System.Collections.Generic.IDictionary接口的泛型字典类中如何使用foreach循环来枚举字典中的键值对。同时还讨论了非泛型字典类和泛型字典类在foreach循环中使用的不同类型,以及使用KeyValuePair类型在foreach循环中枚举泛型字典类的优势。阅读本文可以帮助您更好地理解泛型字典类的使用和性能优化。 ... [详细]
  • HashTable与ConcurrentHashMap均可实现HashMap的功能,对外提供了键值对存储的数据结构。但是在内部结构及实现上有何区别,性能上的差异到底在哪里又是如何导致的 ... [详细]
author-avatar
很想爱到极限
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有