基于.NET的分词软件设计与实现V5.0--使用Hashtable和HashSet<T>提高分词效率

  上篇使用了SortedList,对分词的性能有了显著的改进,但是有一点偶没有提,那就是构造词典的时间,由于SortedList需要保证元素的有序性,所以对于我使用的20+万的词典来说,构造时间也达到了10秒左右,因此与之前的三个版本相比,虽然分词的性能大幅提升,但总的时间并没有什么改进,所以使用SortedList的方案自然也不可行,那让我们看看之前提到的Hashtable表现如何。

一、Hashtable的优势——高效的查找

  时间复杂度:

  这无疑是Hashtable最大的优势所在,对于之前提到的数据结构:ArrayList采用顺序查找,时间复杂度为O(n),而SortedList采用二分查找,时间复杂度为O(logN),而Hashtable则是O(1)。

   原理:

  当Hashtable添加数据时,(例如:hash.Add(key)) 首先调用当前键值key的GetHashCode()方法,得到哈希值(该值为Int32),取该哈希值的绝对值跟内部数组(上面代码的buckets)的总容量进行余运算(就是'%'操作),得到一个小于总容量的值,把该值当做数组序号,存在对应的位置。

  Hashtable之所以查找高效,是因为其存储的元素都是通过键值对的形式进行存储的,需要某个元素的值时,可通过映射Key值,找到数组中对应的索引位置,然后取出元素,整个过程无需循环。

   代价:

  哈希表高效的查找是有代价的,那就是内存,不过,介于现在硬件的发展速度,这种用空间换时间的做法显然没有任何压力。

 

二、Hashtable的不足

  1、哈希表不能有重复键值
  
  这意味着我们在进行哈希表构造时,需要在添加元素时进行判断,若其Key值不存在,则不用重复添加,否则报错。
  
  2、哈希表会占用更多内存
  
    之前提到哈希表高效的查找是以内存为代价的,而原理又是怎样的呢?

  首先,我们实例化ArrayList或List<T>的时候,如果不指定容量,则其内部是赋值为一个静态的空数组。当有添加操作时,会实例化为一个长度为4的数组,如果容量满了以后,再添加,就会自动扩充为两倍的容量。所以使用ArrayList和List <T>的时候,建议在实例化方法里指定容量。

  哈希表也有一个类似的情况,new Hashtable()如果不指定长度,则会将内置bucket数组的长度默认指定为11。如果给定一个值如new Hashtable(20),也并不会将bucket数组长度设置为20,而是循环内部一个素数数组,设置为刚好大于20的素数(此处是23,由此可知,哈希表内部存取数组长度总是一个素数)。

  此外,哈希表在未存满的情况下就要扩充: Hashtable有一个加载因子为0.72f。实际能存储的数据量等于内部长度* 0.72f。也就是说在Hashtable中,假如初始化了100个位置,只有72个可用,其他28个不能用,但仍然要占着地方。由于哈希算法本身的特点,哈希表内部存取数组长度总是一个素数,所以当需要扩充时,新的数组长度是刚好大于原数组二倍长度的素数,也就是说略大于二倍。

 

三、Hashtable——Q&A

  其实,在刚接触Hashtable的时候,就看到一句话,也是上文提到的——哈希表内部存取数组长度总是一个素数,比较不解,如果你和我当时一样有着相同的疑问,那么下面的话希望能够解答。

  哈希表大小为素数在理论上是有依据的,针对不同的哈希算法有不同的证明。例如,对于平方探测法,有如下定理:如果使用平方探测,且表大小是素数,那么当表至少有一半是空的时候,总能够插入一个新的元素。

  hash函数用质数来做模运算(%),分析发现,如果不是用质数来做模运算的话,很多生活中的数据分布会集中在某些点上,所以需要采用质数做模的除数。

      因为用质数做了模的除数,自然存储空间的大小也用质数了,因为模完之后,数据是在[0-所选质数]之间。

 

四、HashSet<T>对于Hashtable的改进

  之前提到的一个Hashtable不允许添加重复值,所以我们构造词典的时候代码会是这样:

  

  可以看到Hashtable是使用键-值方式进行存储的,但有些时候,我们只需要其中一个值,比如上图所示的情况,我只需要词典中的词,对于value我们不需要进行设置,所以这样就造成了内存浪费。

  不过其实有一种数据结构可以改变这样的情况,那就是HashSet<T>,HashSet<T>只保存一个值,所以更加适合处理这种情况。

  HashSet<T>每条数据只保存一项,并不采用Key-Value的方式,换句话说,HashSet<T>中的Key就是Value,假如已经知道了Key,也没必要再查询去获取Value,需要做的只是检查值是否已存在。

  HashSet<T>的Add方法返回bool值,在添加数据时,如果发现集合中已经存在,则忽略这次操作,并返回false值。而Hashtable和Dictionary<TKey,TValue>碰到重复添加的情况会直接抛出错误。

  但HashSet<T>不能使用下标来访问元素,如:hs[1]。

  所以上面的代码就可以写作:

  看着舒心了些吧。。。

 

五、HashSet<T>在个人分词软件中的应用效果

  好了,介绍的差不多,来看下最终的效果吧,以之前的那个完整的测试文本进行测试:

  

  启动毫无压力,运行结果也灰常满意。

 

  到此为此,介绍了个人在分词软件上对于算法、数据结构等方面的研究、测试与改进,最后的这个HashSet<T>的词典版本,也是个人的最终版本。

 

  不过之前的所有测试使用的词典都是以文本形式存储的,下篇将介绍使用数据库进行词典存储后,分词软件的性能表现及相关改进。

 

  本文部分内容参考了园子里面的文章,在此附上友情链接:http://www.cnblogs.com/hkncd/archive/2011/05/06/2035684.html

 

作者:Rocky翔
出处:http://www.cnblogs.com/RockyMyx/
本文版权归作者和博客园共有,欢迎转载,但请在文章明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2011-07-03 09:13  RockyXiang  阅读(2087)  评论(8编辑  收藏  举报