哈希表(HASHTABLE)

原文地址:http://www.cnblogs.com/abatei/archive/2009/06/23/1509790.html 
 

C#中实现了哈希表数据结构的集合类有:
(1) System.Collections.Hashtable
(2) System.Collections.Generic.Dictionary<TKey,TValue>
前者为一般类型的哈希表,后者是泛型版本的哈希表。Dictionary和Hashtable之间并非只是简单的泛型和非泛型的区别,两者使用了完全不同的哈希冲突解决办法。Dictionary我已经做了动态演示程序,使用的是Window应用程序。虽然Dictionary相对于Hashtable来说,更优美、漂亮,但总觉得如果不给Hashtable也配上动态演示程序,也是一种遗憾。这次使用了Silverlight来制作,原因很简单,它可以挂在网上让大家很方便地观看。
先来看看效果,这里需要注意,必须安装Silverlight 2.0 RTW 才能正常运行游戏,下载地址:http://www.microsoft.com/silverlight/resources/install.aspx?v=2.0
程序中的键编辑框中只接受整数,因为整数的哈希码就是整数本身,这可以让大家更直观地查看哈希表的变化。如果输入了非法字符,则会从0至999中随机抽取一个整数进行添加或删除操作。

最新发现不登录博客园的用户无法直接看到Silverlight,如果是这样,请移步到以下网址观看动画:

http://www.bbniu.com/matrix/ShowApplication.aspx?id=148

8.3哈希冲突解决方法

 

哈希函数的目标是尽量减少冲突,但实际应用中冲突是无法避免的,所以在冲突发生时,必须有相应的解决方案。而发生冲突的可能性又跟以下两个因素有关:

(1)       装填因子α:所谓装填因子是指合希表中已存入的记录数n与哈希地址空间大小m的比值,即 α=n / m ,α越小,冲突发生的可能性就越小;α越大(最大可取1),冲突发生的可能性就越大。这很容易理解,因为α越小,哈希表中空闲单元的比例就越大,所以待插入记录同已插入的记录发生冲突的可能性就越小;反之,α越大,哈希表中空闲单元的比例就越小,所以待插入记录同已插入记录冲突的可能性就越大;另一方面,α越小,存储窨的利用率就越低;反之,存储窨的利用率就越高。为了既兼顾减少冲突的发生,又兼顾提高存储空间的利用率,通常把α控制在0.6~0.9的范围之内,C#的HashTable类把α的最大值定为0.72

(2)       与所采用的哈希函数有关。若哈希函数选择得当,就可使哈希地址尽可能均匀地分布在哈希地址空间上,从而减少冲突的发生;否则,就可能使哈希地址集中于某些区域,从而加大冲突发生的可能性。

冲突解决技术可分为两大类:开散列法(又称为链地址法)和闭散列法(又称为开放地址法)。哈希表是用数组实现的一片连续的地址空间,两种冲突解决技术的区别在于发生冲突的元素是存储在这片数组的空间之外还是空间之内:

(1)       开散列法发生冲突的元素存储于数组空间之外。可以把“开”字理解为需要另外“开辟”空间存储发生冲突的元素。

(2)       闭散列法发生冲突的元素存储于数组空间之内。可以把“闭”字理解为所有元素,不管是否有冲突,都“关闭”于数组之中。闭散列法又称开放地址法,意指数组空间对所有元素,不管是否冲突都是开放的。

 

8.3.1  闭散列法(开放地址法)

闭散列法是把所有的元素存储在哈希表数组中。当发生冲突时,在冲突位置的附近寻找可存放记录的空单元。寻找“下一个”空位的过程称为探测。上述方法可用如下公式表示:

hi=(h(key)+di)%m      i=12,…,k (km-1)

其中h(key)为哈希函数;m为哈希表长;di为增量的序列。根据di取值的不同,可以分成几种探测方法,下面只介绍Hashtable所使用到的双重散列法。

  • 双重散列法

双重散列法又称二度哈希,是闭散列法中较好的一种方法,它是以关键字的另一个散列函数值作为增量。设两个哈希函数为:h1h2,则得到的探测序列为:

(h1(key)+h2(key))%m(h1(key)+2h2(key))%m(h1(key)+3h2(key))%m,…

其中,m为哈希表长。由此可知,双重散列法探测下一个开放地址的公式为:

(h1(key) + i * h2(key)) % m     (1im-1)

定义h2的方法较多,但无采用什么方法都必须使h2(key)的值和m互素(又称互质,表示两数的最大公约数为1,或者说是两数没有共同的因子,1除外)才能使发生冲突的同义词地址均匀地分布在整个哈希表中,否则可能造成同义词地址的循环计算。若m为素数,则h21m-1之间的任何数均与m互素,因此可以简单地将h2定义为:

h2(key) = key % (m - 2) + 1

 

 8.4 剖析System.Collections.Hashtable

 

万物之母object类中定义了一个GetHashCode()方法,这个方法默认的实现是返回一个唯一的整数值以保证在object的生命期中不被修改。既然每种类型都是直接或间接从object派生的,因此所有对象都可以访问该方法。自然,字符串或其他类型都能以唯一的数字值来表示。也就是说,GetHashCode()方法使得所有对象的哈希函数构造方法都趋于统一。当然,由于GetHashCode()方法是一个虚方法,你也可以通过重写这个方法来构造自己的哈希函数。

 

8.4.1  Hashtable的实现原理

Hashtable使用了闭散列法来解决冲突,它通过一个结构体bucket来表示哈希表中的单个元素,这个结构体中有三个成员:

(1)       key :表示键,即哈希表中的关键字。

(2)       val :表示值,即跟关键字所对应值。

(3)       hash_coll :它是一个int类型,用于表示键所对应的哈希码。

int类型占据32个位的存储空间,它的最高位是符号位,为“0”时,表示这是一个正整数;为“1”时表示负整数。hash_coll使用最高位表示当前位置是否发生冲突,为“0”时,也就是为正数时,表示未发生冲突;为“1”时,表示当前位置存在冲突。之所以专门使用一个位用于存放哈希码并标注是否发生冲突,主要是为了提高哈希表的运行效率。关于这一点,稍后会提到。

Hashtable解决冲突使用了双重散列法,但又跟前面所讲的双重散列法稍有不同。它探测地址的方法如下:

h(key, i) = h1(key) + i * h2(key)

其中哈希函数h1h2的公式如下:

h1(key) = key.GetHashCode()

h2(key) = 1 + (((h1(key) >> 5) + 1) % (hashsize - 1))

由于使用了二度哈希,最终的h(key, i)的值有可能会大于hashsize,所以需要对h(key, i)进行模运算,最终计算的哈希地址为:

哈希地址 = h(key, i) % hashsize

【注意】:bucket结构体的hash_coll字段所存储的是h(key, i)的值而不是哈希地址。

哈希表的所有元素存放于一个名称为buckets(又称为数据桶) bucket数组之中,下面演示一个哈希表的数据的插入和删除过程,其中数据元素使用(键,值,哈希码)来表示。注意,本例假设Hashtable的长度为11hashsize = 11,这里只显示其中的前5个元素。

(1)       插入元素(k1v11)和(k2v22)。

由于插入的两个元素不存在冲突,所以直接使用h1(key) % hashsize的值做为其哈希码而忽略了h2(key)。其效果如图8.6所示。

 

(2)      插入元素(k3v312

     新插入的元素的哈希码为12,由于哈希表长为1112 % 11 = 1,所以新元素应该插入到索引1处,但由于索引1处已经被k1占据,所以需要使用h2(key)重新计算哈希码。

h2(key) = 1 + (((h1(key) >> 5) + 1) % (hashsize - 1))

h2(key) = 1 + ((12 >> 5) + 1) % (11 - 1)) = 2

新的哈希地址为 h1(key) + i * h2(key) = 1 + 1 * 2 = 3,所以k3插入到索引3处。而由于索引1处存在冲突,所以需要置其最高位为“1”。

(10000000000000000000000000000001)2 = (-2147483647)10

最终效果如图8.7所示。

 

(3)       插入元素(k4v414

k4的哈希码为1414 % 11 = 3,而索引3处已被k3占据,所以使用二度哈希重新计算地址,得到新地址为14。索引3处存在冲突,所以需要置高位为“1”。

(12)10 = (00000000000000000000000000001100)2   高位置“1”后

(10000000000000000000000000001100)2 = (-2147483636)10

最终效果如图8.8所示。

 

(4)       删除元素k1k2

Hashtable在删除一个存在冲突的元素时(hash_coll为负数),会把这个元素的key指向数组buckets,同时将该元素的hash_coll的低31位全部置“0”而保留最高位,由于原hash_coll为负数,所以最高位为“1”。

(10000000000000000000000000000000)2 = (-2147483648)10

单凭判断hash_coll的值是否为-2147483648无法判断某个索引处是否为空,因为当索引0处存在冲突时,它的hash_coll的值同样也为-2147483648,这也是为什么要把key指向buckets的原因。这里把key指向buckets并且hash_coll值为-2147483648的空位称为“有冲突空位”。如图8.8所示,当k1被删除后,索引1处的空位就是有冲突空位。

Hashtable在删除一个不存在冲突的元素时(hash_coll为正数),会把键和值都设为nullhash_coll的值设为0。这种没有冲突的空位称为“无冲突空位”,如图8.9所示,k2被删除后索引2处就属于无冲突空位,当一个Hashtable被初始化后,buckets数组中的所有位置都是无冲突空位。

 

哈希表通过关键字查找元素时,首先计算出键的哈希地址,然后通过这个哈希地址直接访问数组的相应位置并对比两个键值,如果相同,则查找成功并返回;如果不同,则根据hash_coll的值来决定下一步操作。当hash_coll0或正数时,表明没有冲突,此时查找失败;如果hash_coll为负数时,表明存在冲突,此时需通过二度哈希继续计算哈希地址进行查找,如此反复直到找到相应的键值表明查找成功,如果在查找过程中遇到hash_coll为正数或计算二度哈希的次数等于哈希表长度则查找失败。由此可知,将hash_coll的高位设为冲突位主要是为了提高查找速度,避免无意义地多次计算二度哈希的情况。

 

8.4.2  Hashtable的代码实现

哈希表的实现较为复杂,为了简化代码,本例忽略了部分出错判断,在测试时请不要设key值为空。

 

1     using  System;
2     public  class  Hashtable
3    {
4         private  struct  bucket
5        {
6             public  Object key;  //
7             public  Object val;  //
8             public  int  hash_coll;  // 哈希码
9        }
10        private  bucket[] buckets;  // 存储哈希表数据的数组(数据桶)
11        private  int  count;  // 元素个数
12        private  int  loadsize;  // 当前允许存储的元素个数
13        private  float  loadFactor;  // 填充因子
14        // 默认构造方法
15        public  Hashtable() :  this ( 0 1.0f ) { }
16        // 指定容量的构造方法
17        public  Hashtable( int  capacity,  float  loadFactor)
18       {
19            if  ( ! (loadFactor  >=  0.1f  &&  loadFactor  <=  1.0f ))
20                throw  new  ArgumentOutOfRangeException(
21                    " 填充因子必须在0.1~1之间 " );
22            this .loadFactor  =  loadFactor  >  0.72f  ?  0.72f  : loadFactor;
23            // 根据容量计算表长
24            double  rawsize  =  capacity  /  this .loadFactor;
25            int  hashsize  =  (rawsize  >  11 ?  // 表长为大于11的素数
26               HashHelpers.GetPrime(( int )rawsize) :  11 ;
27           buckets  =  new  bucket[hashsize];  // 初始化容器
28           loadsize  =  ( int )( this .loadFactor  *  hashsize);
29       }
30        public  virtual  void  Add(Object key, Object value)  // 添加
31       {   
32           Insert(key, value,  true );
33       }
34        // 哈希码初始化
35        private  uint  InitHash(Object key, int  hashsize,
36            out  uint  seed, out  uint  incr)
37       {
38            uint  hashcode  =  ( uint )GetHash(key)  &  0x7FFFFFFF // 取绝对值
39           seed  =  ( uint )hashcode;  // h1
40           incr  =  ( uint )( 1  +  (((seed  >>  5 ) + 1 %  (( uint )hashsize - 1 ))); // h2
41            return  hashcode;  // 返回哈希码
42       }
43        public  virtual  Object  this [Object key]  // 索引器
44       {
45            get
46           {
47                uint  seed;  // h1
48                uint  incr;  // h2
49                uint  hashcode  =  InitHash(key, buckets.Length, 
50                    out  seed,  out  incr);
51                int  ntry  =  0 // 用于表示h(key,i)中的i值
52               bucket b;
53                int  bn  =  ( int )(seed  %  ( uint )buckets.Length);  // h(key,0)
54                do
55               {
56                   b  =  buckets[bn]; 
57                    if  (b.key  ==  null // b为无冲突空位时
58                   {   // 找不到相应的键,返回空
59                        return  null ;
60                   }
61                    if  (((b.hash_coll  &  0x7FFFFFFF ==  hashcode)  &&
62                       KeyEquals(b.key, key))
63                   {    // 查找成功
64                        return  b.val;
65                   }
66                   bn  =  ( int )((( long )bn  +  incr)  %  
67                       ( uint )buckets.Length);  // h(key+i)
68               }  while  (b.hash_coll  <  0  &&  ++ ntry  <  buckets.Length);
69                return  null ;
70           }
71            set
72           {
73               Insert(key, value,  false );
74           }
75       }
76        private  void  expand()  // 扩容
77       {    // 使新的容量为旧容量的近似两倍
78            int  rawsize  =  HashHelpers.GetPrime(buckets.Length  *  2 ); 
79           rehash(rawsize);
80       }
81        private  void  rehash( int  newsize)  // 按新容量扩容
82       {
83           bucket[] newBuckets  =  new  bucket[newsize];
84            for  ( int  nb  =  0 ; nb  <  buckets.Length; nb ++ )
85           {
86               bucket oldb  =  buckets[nb];
87                if  ((oldb.key  !=  null &&  (oldb.key  !=  buckets))
88               {
89                   putEntry(newBuckets, oldb.key, oldb.val, 
90                       oldb.hash_coll  &  0x7FFFFFFF );
91               }
92           }
93           buckets  =  newBuckets;
94           loadsize  =  ( int )(loadFactor  *  newsize);
95            return ;
96       }
97        // 在新数组内添加旧数组的一个元素
98        private  void  putEntry(bucket[] newBuckets, Object key, 
99           Object nvalue,  int  hashcode)
100      {
101           uint  seed  =  ( uint )hashcode;  // h1
102           uint  incr  =  ( uint )( 1  +  (((seed  >>  5 +  1 %  
103              (( uint )newBuckets.Length  -  1 )));  // h2
104           int  bn  =  ( int )(seed  %  ( uint )newBuckets.Length); // 哈希地址
105           do
106          {    // 当前位置为有冲突空位或无冲突空位时都可添加新元素
107               if  ((newBuckets[bn].key  ==  null ||  
108                  (newBuckets[bn].key  ==  buckets))
109              {    // 赋值
110                  newBuckets[bn].val  =  nvalue;
111                  newBuckets[bn].key  =  key;
112                  newBuckets[bn].hash_coll  |=  hashcode;
113                   return ;
114              }
115               // 当前位置已存在其他元素时
116               if  (newBuckets[bn].hash_coll  >=  0 )
117              {    // 置hash_coll的高位为1
118                  newBuckets[bn].hash_coll  |=  
119                       unchecked (( int ) 0x80000000 );
120              }
121               // 二度哈希h1(key)+h2(key)
122              bn  =  ( int )((( long )bn  +  incr)  %  ( uint )newBuckets.Length);
123          }  while  ( true );
124      }
125       protected  virtual  int  GetHash(Object key)
126      {    // 获取哈希码
127           return  key.GetHashCode();
128      }
129       protected  virtual  bool  KeyEquals(Object item, Object key)
130      {    // 用于判断两key是否相等
131           return  item  ==  null  ?  false  : item.Equals(key);
132      }
133       // 当add为true时用作添加元素,当add为false时用作修改元素值
134       private  void  Insert(Object key, Object nvalue,  bool  add)
135      {    // 如果超过允许存放元素个数的上限则扩容
136           if  (count  >=  loadsize)
137          {   
138              expand();
139          }
140           uint  seed;  // h1
141           uint  incr;  // h2
142           uint  hashcode  =  InitHash(key, buckets.Length, out  seed,  out  incr);
143           int  ntry  =  0 // 用于表示h(key,i)中的i值
144           int  emptySlotNumber  =  - 1 // 用于记录空位
145           int  bn  =  ( int )(seed  %  ( uint )buckets.Length);  // 索引号
146           do
147          {    // 如果是有冲突空位,需继续向后查找以确定是否存在相同的键
148               if  (emptySlotNumber  ==  - 1  &&  (buckets[bn].key  ==  buckets)  &&
149                  (buckets[bn].hash_coll  <  0 ))
150              {
151                  emptySlotNumber  =  bn;
152              }
153               if  (buckets[bn].key  ==  null // 确定没有重复键才添加
154              {
155                   if  (emptySlotNumber  !=  - 1 // 使用之前的空位
156                      bn  =  emptySlotNumber;
157                  buckets[bn].val  =  nvalue;
158                  buckets[bn].key  =  key;
159                  buckets[bn].hash_coll  |=  ( int )hashcode;
160                  count ++ ;
161                   return ;
162              }
163               // 找到重复键
164               if  (((buckets[bn].hash_coll  &  0x7FFFFFFF ) == hashcode)  &&
165                  KeyEquals(buckets[bn].key, key))
166              {    // 如果处于添加元素状态,则由于出现重复键而报错
167                   if  (add)
168                  {
169                       throw  new  ArgumentException( " 添加了重复的键值! " );
170                  }
171                  buckets[bn].val  =  nvalue;  // 修改批定键的元素
172                   return ;
173              }
174               // 存在冲突则置hash_coll的最高位为1
175               if  (emptySlotNumber  ==  - 1 )
176              {
177                   if  (buckets[bn].hash_coll  >=  0 )
178                  {
179                      buckets[bn].hash_coll  |=  unchecked (( int ) 0x80000000 );
180                  }
181              }
182              bn  =  ( int )((( long )bn  +  incr)  %  ( uint )buckets.Length); // 二度哈希
183          }  while  ( ++ ntry  <  buckets.Length);
184           throw  new  InvalidOperationException( " 添加失败! " );
185      }
186       public  virtual  void  Remove(Object key)  // 移除一个元素
187      {
188           uint  seed;  // h1
189           uint  incr;  // h2
190           uint  hashcode  =  InitHash(key, buckets.Length, out  seed,  out  incr);
191           int  ntry  =  0 // h(key,i)中的i
192          bucket b;
193           int  bn  =  ( int )(seed  %  ( uint )buckets.Length);  // 哈希地址
194           do
195          {
196              b  =  buckets[bn];
197               if  (((b.hash_coll  &  0x7FFFFFFF ==  hashcode)  &&
198                  KeyEquals(b.key, key))  // 如果找到相应的键值
199              {    // 保留最高位,其余清0
200                  buckets[bn].hash_coll  &=  unchecked (( int ) 0x80000000 );
201                   if  (buckets[bn].hash_coll  !=  0 // 如果原来存在冲突
202                  {    // 使key指向buckets
203                      buckets[bn].key  =  buckets;
204                  }
205                   else  // 原来不存在冲突
206                  {    // 置key为空
207                      buckets[bn].key  =  null ;
208                  }
209                  buckets[bn].val  =  null ;   // 释放相应的“值”。
210                  count -- ;
211                   return ;
212              }  // 二度哈希
213              bn  =  ( int )((( long )bn  +  incr)  %  ( uint )buckets.Length);
214          }  while  (b.hash_coll  <  0  &&  ++ ntry  <  buckets.Length);
215      }
216       public  override  string  ToString()
217      {
218           string  s  =  string .Empty;
219           for  ( int  i  =  0 ; i  <  buckets.Length; i ++ )
220          {
221               if  (buckets[i].key  !=  null  &&  buckets[i].key  !=  buckets)
222              {    // 不为空位时打印索引、键、值、hash_coll
223                  s  +=  string .Format( " {0,-5}{1,-8}{2,-8}{3,-8}\r\n " ,
224                      i.ToString(), buckets[i].key.ToString(),
225                      buckets[i].val.ToString(), 
226                      buckets[i].hash_coll.ToString());
227              }
228               else
229              {    // 是空位时则打印索引和hash_coll
230                  s  +=  string .Format( " {0,-21}{1,-8}\r\n " , i.ToString(),
231                      buckets[i].hash_coll.ToString());
232              }
233          }
234           return  s;
235      }
236       public  virtual  int  Count  // 属性
237      {    // 获取元素个数
238           get  {  return  count; }
239      }
240  }
Hashtable ArrayList 的实现有似的地方,比如两者都是以数组为基础做进一步地抽象而来,两者都可以成倍地自动扩展容量。 

转载于:https://www.cnblogs.com/jeriffe/articles/2433943.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值