原文链接: http://blog.csdn.net/snakorse/article/details/43956061
笔记:
hash表基础
hash冲突解决方法:开散列法,冲突后用开辟新地址存放,如用链接,闭散列法,在原来数组中寻找下一个地址,又称开放地址法。
c# hashtable
1. hash表构成:一个数组,bucket,个数起始为3,装填因子是0.72,若插入时超过装填因子,则重新扩容为下一个素数,重新计算现有所有的数据的hash。
2. bucket结构包括 key,value,hashcode。hashcode存放key的hash值,最高位用来表示有无冲突,发生冲突时,把最高位置位
3. hash函数采用二度hash算法 h(key,i)=h1(key)+h2(key)*i , h2(key) = 1 + (((h1(key) >> 5) + 1) % (hashsize - 1)), 最红索引位置为h(key,i)%hashsize
4. 插入过程:若无冲突,直接插入对应索引处,若有冲突,先将冲突位hashcode最高位置位,然后再计算新的hashcode,直到最后无冲突。
5. 查找过程:计算hashcode,找到索引位,比较key,若相同则找到,若不相同,如果此位置没有冲突,则查找失败,若此位置有冲突,再次计算hashcode,比较下一个索引位置的key,直到最后查找成功或失败
6. 删除过程:如果查找成功,如果没冲突,则将key,value置null,hashcode置0,如果有冲突,key为bucket,value为null,hashcode低位置0
先例举几个问题:1,Hashtable为什么速度查询速度快,而添加速度相对慢,且其添加和查询速度之比相差一个数量等级?
2,装填因子( Load Factor)是什么,hashtable默认的装填因子是多少?
3,hashtable里的元素是顺序排序的吗?
4,hashtable内部的数据桶(数组)默认长度多少,其长度为什么只能是素数?
Hashtable中的数据实际存储在内部的一个数据桶里(bucket结构数组),其和普通的数组一样,容量固定,根据数组索引获取值。
下面从正常使用Hashtable场景看内部是如何实现的,内部都做了哪些工作。
一,new一个Hashtable,Hashtable ht=new Hashtable();
Hashtable有多个构造函数,常用的是无参构造函数:Hashtable ht=new Hashtable(),在new一个hashtable时,其内部做了如下工作:调用Hashtable(int capacity,float loadFactor),其中capacity为:0,loadFactor为:1,然后初始化bocket数组大小为3,装载因子为0.72(该值是微软权衡后给的值),如下图所示,该图截取Reflector
二,向Hashtable添加一个元素,ht.Add("a","123")
1,判断当前Hashtable :ht的元素个数与bucket数组之比是否超过装载因子0.72,
1)小于0.72:对a进行哈希取值,然后将得到的值与bucket数组长度进行取模计算,将取模的结果插入到bucket数组对应索引,将“123”赋值其value.
因为哈希值可能会重复(不明白的百度一下),从而导致地址冲突,Hashtable 采用的是 "开放定址法" 处理冲突, 具体行为是把 HashOf(k) % Array.Length 改为 (HashOf(k) + d(k)) % Array.Length , 得出另外一个位置来存储关键字 "a" 所对应的数据, d 是一个增量函数. 如果仍然冲突, 则再次进行增量, 依此循环直到找到一个 Array 中的空位为止。
2) 大于0.72:对bucket数组进行扩容,a, 新建一个数组(该数组的大小为两倍于当前容量最小的素数,比如当前数组长度是3,那么新数组长度为7)。
b,将原来数组元素拷贝到新的数组中,因为bocket数组长度变了,所以需要对所有key重新哈希计算(这是影响hashtable性能的重要因素)。
c, 进行上面a步骤。
三,通过key获取Hashtable对应的value,var v=ht["a"];
1) 计算"a"的哈希值。
2)将计算的结果与bocket数组长度进行取模计算,因为哈希值可能会冲突,所以类似定位索引上的key可能与输入的key不相同,这时继续查找下一个位置。。。。。
3)取模计算结果即是存储在bocket数组上"123"的索引。
Hashtable还有很多方法,比如Clear ,Remove ,ContainsKey,ContainsValue等方法,因篇幅有限这里就不一一介绍了。
写到这里来回答一下篇幅开头的几个问题。
1,Hashtable查询速度快是因为内部是基于数组索引定位的,稍微消耗性能的是取KEY的哈希值,添加性能相对查询慢是因为:a,添加元素时可能会地址冲突,需要重新定位地址 。 b,扩容后 数组拷贝,重新哈希计算旧数组所有key。
2, 装填因子是Hashtable“已存元素个数/内部bucket数组长度”,这个比值太大会造成冲突概率增大,太小会造成空间的浪费。默认是0.72,该值是微软经过大量实验得出的一个比较平衡的值,装填因子范围 0.1<loadFactor<1,否则抛出ArgumentOutOfRangeException异常。
3,不是顺序的(各位看了文章应该知道为什么不是顺序的了吧?)
4,默认长度是3,我看的是.net framework 4.5版本反编译的代码,其他版本的.net framework不确定是不是这个值。为什么扩容的数组长度一定要是素数呢?因为素数有一个特点,只能被自己和1整除,如果不是素数那么在进行取模计算的时候可能会出现多个值。
===========================================================
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=1,2,…,k (k≤m-1)
其中h(key)为哈希函数;m为哈希表长;di为增量的序列。根据di取值的不同,可以分成几种探测方法,下面只介绍Hashtable所使用到的双重散列法。
双重散列法又称二度哈希,是闭散列法中较好的一种方法,它是以关键字的另一个散列函数值作为增量。设两个哈希函数为:h1和h2,则得到的探测序列为:
(h1(key)+h2(key))%m,(h1(key)+2h2(key))%m,(h1(key)+3h2(key))%m,…
其中,m为哈希表长。由此可知,双重散列法探测下一个开放地址的公式为:
(h1(key) + i * h2(key)) % m (1≤i≤m-1)
定义h2的方法较多,但无采用什么方法都必须使h2(key)的值和m互素(又称互质,表示两数的最大公约数为1,或者说是两数没有共同的因子,1除外)才能使发生冲突的同义词地址均匀地分布在整个哈希表中,否则可能造成同义词地址的循环计算。若m为素数,则h2取1至m-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)
其中哈希函数h1和h2的公式如下:
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的长度为11,即hashsize = 11,这里只显示其中的前5个元素。
(1) 插入元素(k1,v1,1)和(k2,v2,2)。
由于插入的两个元素不存在冲突,所以直接使用h1(key) % hashsize的值做为其哈希码而忽略了h2(key)。其效果如图8.6所示。
(2) 插入元素(k3,v3,12)
新插入的元素的哈希码为12,由于哈希表长为11,12 % 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) 插入元素(k4,v4,14)
k4的哈希码为14,14 % 11 = 3,而索引3处已被k3占据,所以使用二度哈希重新计算地址,得到新地址为14。索引3处存在冲突,所以需要置高位为“1”。
(12)10 = (00000000000000000000000000001100)2 高位置“1”后
(10000000000000000000000000001100)2 = (-2147483636)10
最终效果如图8.8所示。
(4) 删除元素k1和k2
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为正数),会把键和值都设为null,hash_coll的值设为0。这种没有冲突的空位称为“无冲突空位”,如图8.9所示,k2被删除后索引2处就属于无冲突空位,当一个Hashtable被初始化后,buckets数组中的所有位置都是无冲突空位。
哈希表通过关键字查找元素时,首先计算出键的哈希地址,然后通过这个哈希地址直接访问数组的相应位置并对比两个键值,如果相同,则查找成功并返回;如果不同,则根据hash_coll的值来决定下一步操作。当hash_coll为0或正数时,表明没有冲突,此时查找失败;如果hash_coll为负数时,表明存在冲突,此时需通过二度哈希继续计算哈希地址进行查找,如此反复直到找到相应的键值表明查找成功,如果在查找过程中遇到hash_coll为正数或计算二度哈希的次数等于哈希表长度则查找失败。由此可知,将hash_coll的高位设为冲突位主要是为了提高查找速度,避免无意义地多次计算二度哈希的情况。
8.4.2 Hashtable的代码实现
哈希表的实现较为复杂,为了简化代码,本例忽略了部分出错判断,在测试时请不要设key值为空。
- using System;
- public class Hashtable
- {
- private struct bucket
- {
- public Object key;
- public Object val;
- public int hash_coll;
- }
- private bucket[] buckets;
- private int count;
- private int loadsize;
- private float loadFactor;
-
- public Hashtable() : this(0, 1.0f) { }
-
- public Hashtable(int capacity, float loadFactor)
- {
- if (!(loadFactor >= 0.1f && loadFactor <= 1.0f))
- throw new ArgumentOutOfRangeException(
- "填充因子必须在0.1~1之间");
- this.loadFactor = loadFactor > 0.72f ? 0.72f : loadFactor;
-
- double rawsize = capacity / this.loadFactor;
- int hashsize = (rawsize > 11) ?
- HashHelpers.GetPrime((int)rawsize) : 11;
- buckets = new bucket[hashsize];
- loadsize = (int)(this.loadFactor * hashsize);
- }
- public virtual void Add(Object key, Object value)
- {
- Insert(key, value, true);
- }
-
- private uint InitHash(Object key,int hashsize,
- out uint seed,out uint incr)
- {
- uint hashcode = (uint)GetHash(key) & 0x7FFFFFFF;
- seed = (uint)hashcode;
- incr = (uint)(1 + (((seed >> 5)+1) % ((uint)hashsize-1)));
- return hashcode;
- }
- public virtual Object this[Object key]
- {
- get
- {
- uint seed;
- uint incr;
- uint hashcode = InitHash(key, buckets.Length,
- out seed, out incr);
- int ntry = 0;
- bucket b;
- int bn = (int)(seed % (uint)buckets.Length);
- do
- {
- b = buckets[bn];
- if (b.key == null)
- {
- return null;
- }
- if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
- KeyEquals(b.key, key))
- {
- return b.val;
- }
- bn = (int)(((long)bn + incr) %
- (uint)buckets.Length);
- } while (b.hash_coll < 0 && ++ntry < buckets.Length);
- return null;
- }
- set
- {
- Insert(key, value, false);
- }
- }
- private void expand()
- {
- int rawsize = HashHelpers.GetPrime(buckets.Length * 2);
- rehash(rawsize);
- }
- private void rehash(int newsize)
- {
- bucket[] newBuckets = new bucket[newsize];
- for (int nb = 0; nb < buckets.Length; nb++)
- {
- bucket oldb = buckets[nb];
- if ((oldb.key != null) && (oldb.key != buckets))
- {
- putEntry(newBuckets, oldb.key, oldb.val,
- oldb.hash_coll & 0x7FFFFFFF);
- }
- }
- buckets = newBuckets;
- loadsize = (int)(loadFactor * newsize);
- return;
- }
-
- private void putEntry(bucket[] newBuckets, Object key,
- Object nvalue, int hashcode)
- {
- uint seed = (uint)hashcode;
- uint incr = (uint)(1 + (((seed >> 5) + 1) %
- ((uint)newBuckets.Length - 1)));
- int bn = (int)(seed % (uint)newBuckets.Length);
- do
- {
- if ((newBuckets[bn].key == null) ||
- (newBuckets[bn].key == buckets))
- {
- newBuckets[bn].val = nvalue;
- newBuckets[bn].key = key;
- newBuckets[bn].hash_coll |= hashcode;
- return;
- }
-
- if (newBuckets[bn].hash_coll >= 0)
- {
- newBuckets[bn].hash_coll |=
- unchecked((int)0x80000000);
- }
-
- bn = (int)(((long)bn + incr) % (uint)newBuckets.Length);
- } while (true);
- }
- protected virtual int GetHash(Object key)
- {
- return key.GetHashCode();
- }
- protected virtual bool KeyEquals(Object item, Object key)
- {
- return item == null ? false : item.Equals(key);
- }
-
- private void Insert(Object key, Object nvalue, bool add)
- {
- if (count >= loadsize)
- {
- expand();
- }
- uint seed;
- uint incr;
- uint hashcode = InitHash(key, buckets.Length,out seed, out incr);
- int ntry = 0;
- int emptySlotNumber = -1;
- int bn = (int)(seed % (uint)buckets.Length);
- do
- {
- if (emptySlotNumber == -1 && (buckets[bn].key == buckets) &&
- (buckets[bn].hash_coll < 0))
- {
- emptySlotNumber = bn;
- }
- if (buckets[bn].key == null)
- {
- if (emptySlotNumber != -1)
- bn = emptySlotNumber;
- buckets[bn].val = nvalue;
- buckets[bn].key = key;
- buckets[bn].hash_coll |= (int)hashcode;
- count++;
- return;
- }
-
- if (((buckets[bn].hash_coll & 0x7FFFFFFF)==hashcode) &&
- KeyEquals(buckets[bn].key, key))
- {
- if (add)
- {
- throw new ArgumentException("添加了重复的键值!");
- }
- buckets[bn].val = nvalue;
- return;
- }
-
- if (emptySlotNumber == -1)
- {
- if (buckets[bn].hash_coll >= 0)
- {
- buckets[bn].hash_coll |= unchecked((int)0x80000000);
- }
- }
- bn = (int)(((long)bn + incr) % (uint)buckets.Length);
- } while (++ntry < buckets.Length);
- throw new InvalidOperationException("添加失败!");
- }
- public virtual void Remove(Object key)
- {
- uint seed;
- uint incr;
- uint hashcode = InitHash(key, buckets.Length,out seed, out incr);
- int ntry = 0;
- bucket b;
- int bn = (int)(seed % (uint)buckets.Length);
- do
- {
- b = buckets[bn];
- if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
- KeyEquals(b.key, key))
- {
- buckets[bn].hash_coll &= unchecked((int)0x80000000);
- if (buckets[bn].hash_coll != 0)
- {
- buckets[bn].key = buckets;
- }
- else
- {
- buckets[bn].key = null;
- }
- buckets[bn].val = null;
- count--;
- return;
- }
- bn = (int)(((long)bn + incr) % (uint)buckets.Length);
- } while (b.hash_coll < 0 && ++ntry < buckets.Length);
- }
- public override string ToString()
- {
- string s = string.Empty;
- for (int i = 0; i < buckets.Length; i++)
- {
- if (buckets[i].key != null && buckets[i].key != buckets)
- {
- s += string.Format("{0,-5}{1,-8}{2,-8}{3,-8}\r\n",
- i.ToString(), buckets[i].key.ToString(),
- buckets[i].val.ToString(),
- buckets[i].hash_coll.ToString());
- }
- else
- {
- s += string.Format("{0,-21}{1,-8}\r\n", i.ToString(),
- buckets[i].hash_coll.ToString());
- }
- }
- return s;
- }
- public virtual int Count
- {
- get { return count; }
- }
- }
Hashtable和ArrayList的实现有似的地方,比如两者都是以数组为基础做进一步地抽象而来,两者都可以成倍地自动扩展容量。
HashTable<T>泛型类的代码实现
开放地址法 Xn=(Xn-1 +b ) % size
理论上b要和size是要精心选择的,不过我这边没有做特别的处理,101的默认size是从c#源代码中抄袭的。。。。
代码尽量简单一点是为了理解方便
hashtable快满的时候扩展一倍空间,数据和标志位还有key 这三个数组都要扩展
删除的时候不能直接删除元素,只能打一个标志(因为用了开放地方方法)
目前只支持string和int类型的key(按位131进制)
非线程安全- 因为这是范例代码
支持泛型
- public class Hashtable<T>
-
- {
- public Hashtable()
- {
- this.dataArray = new T[this.m];
- this.avaiableCapacity = this.m;
- this.keyArray = new int[this.m];
- for (int i = 0; i < this.keyArray.Length; i++)
- {
- this.keyArray[i] = -1;
- }
- this.flagArray = new bool[this.m];
- }
-
- private int m = 101;
-
- private int l = 1;
-
- private int avaiableCapacity;
-
- private double factor = 0.35;
-
- private T[] dataArray;
-
- private int[] keyArray;
-
- private bool[] flagArray;
-
- public void Add(string s, T item)
- {
- if (string.IsNullOrEmpty(s))
- {
- throw new ArgumentNullException("s");
- }
-
- if ((double)this.avaiableCapacity / this.m < this.factor)
- {
- this.ExtendCapacity();
- }
-
- var code = HashtableHelper.GetStringHash(s);
- this.AddItem(code, item, this.dataArray, code, this.keyArray, this.flagArray);
- }
-
- public T Get(string s)
- {
- if (string.IsNullOrEmpty(s))
- {
- throw new ArgumentNullException("s");
- }
-
- var code = HashtableHelper.GetStringHash(s);
- return this.GetItem(code, this.dataArray, code, this.keyArray, this.flagArray);
- }
-
- private void ExtendCapacity()
- {
- this.m *= 2;
- this.avaiableCapacity += this.m;
- T[] newItems = new T[this.m];
- int[] newKeys = new int[this.m];
- bool[] newFlags = new bool[this.m];
-
- for (int i = 0; i < newKeys.Length; i++)
- {
- newKeys[i] = -1;
- }
-
- for (int i = 0; i < this.dataArray.Length; i++)
- {
- if (this.keyArray[i] >= 0 && !this.flagArray[i])
- {
-
- this.AddItem(
- this.keyArray[i],
- this.dataArray[i],
- newItems,
- this.keyArray[i],
- newKeys,
- this.flagArray);
- }
- }
- this.dataArray = newItems;
- this.keyArray = newKeys;
- this.flagArray = newFlags;
-
- }
-
- private int AddItem(int code, T item, T[] data, int hashCode, int[] keys, bool[] flags)
- {
- int address = code % this.m;
- if (keys[address] < 0)
- {
- data[address] = item;
- keys[address] = hashCode;
- this.avaiableCapacity--;
- return address;
- }
- else if (keys[address] == hashCode)
- {
- if (flags[address])
- {
- flags[address] = false;
- data[address] = item;
- return address;
- }
- throw new ArgumentException("duplicated key");
- }
- else
- {
- int nextAddress = address + this.l;
- return this.AddItem(nextAddress, item, data, hashCode, keys, flags);
- }
- }
-
- private T GetItem(int code, T[] data, int hashCode, int[] keys, bool[] flags)
- {
- int address = code % this.m;
- if (keys[address] < 0)
- {
- return default(T);
- }
- else if (keys[address] == hashCode)
- {
- if (flags[address])
- {
- return default(T);
- }
- return data[address];
- }
- else
- {
- int nextAddress = address + this.l;
- return this.GetItem(nextAddress, data, hashCode, keys, flags);
- }
- }
-
- public void Delete(string s)
- {
- if (string.IsNullOrEmpty(s))
- {
- throw new ArgumentNullException("s");
- }
-
- var code = HashtableHelper.GetStringHash(s);
- this.DeleteItem(code, this.dataArray, code, this.keyArray, this.flagArray);
- }
-
- private void DeleteItem(int code, T[] data, int hashCode, int[] keys, bool[] flags)
- {
- int address = code % this.m;
- if (keys[address] < 0)
- {
- return;
-
- }
- else if (keys[address] == hashCode)
- {
- if (!this.flagArray[address])
- {
- flags[address] = true;
- this.avaiableCapacity++;
- }
- }
- else
- {
- int nextAddress = address + this.l;
- this.DeleteItem(nextAddress, data, hashCode, keys, flags);
- }
- }
- }
-
-
- public class HashtableHelper
- {
- public static int GetStringHash(string s)
- {
- if (string.IsNullOrEmpty(s))
- {
- throw new ArgumentNullException("s");
- }
-
- var bytes = Encoding.ASCII.GetBytes(s);
- int checksum = GetBytesHash(bytes, 0, bytes.Length);
- return checksum;
- }
-
- public static int GetBytesHash(byte[] array, int ibStart, int cbSize)
- {
- if (array == null || array.Length == 0)
- {
- throw new ArgumentNullException("array");
- }
-
- int checksum = 0;
- for (int i = ibStart; i < (ibStart + cbSize); i++)
- {
- checksum = (checksum * 131) + array[i];
- }
- return checksum;
- }
-
- public static int GetBytesHash(char[] array, int ibStart, int cbSize)
- {
- if (array == null || array.Length == 0)
- {
- throw new ArgumentNullException("array");
- }
-
- int checksum = 0;
- for (int i = ibStart; i < (ibStart + cbSize); i++)
- {
- checksum = (checksum * 131) + array[i];
- }
- return checksum;
- }
- }
解决哈希(HASH)冲突的主要方法
虽然我们不希望发生冲突,但实际上发生冲突的可能性仍是存在的。当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时。冲突就难免会发 生。另外,当关键字的实际取值大于哈希表的长度时,而且表中已装满了记录,如果插入一个新记录,不仅发生冲突,而且还会发生溢出。因此,处理冲突和溢出是 哈希技术中的两个重要问题。
1、开放定址法
用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。
注意:
①用开放定址法建立散列表时,建表前须将表中所有单元(更严格地说,是指单元中存储的关键字)置空。
②空单元的表示与具体的应用相关。
按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、线性补偿探测法、随机探测等。
(1)线性探查法(Linear Probing)
该方法的基本思想是:
将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
d,d+l,d+2,…,m-1,0,1,…,d-1
即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
探查过程终止于三种情况:
(1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);
(2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
(3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
利用开放地址法的一般形式,线性探查法的探查序列为:
hi=(h(key)+i)%m 0≤i≤m-1 //即di=i
用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:
① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简单的结构是顺序表,查找方法可用顺序查找。
② 按上述算法建立起来的哈希表,删除工作非常困难。假如要从哈希表 HT 中删除一个记录,按理应将这个记录所在位置置为空,但我们不能这样做,而只能标上已被删除的标记,否则,将会影响以后的查找。
③ 线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。
(2)线性补偿探测法
线性补偿探测法的基本思想是:
将线性探测的步长从 1 改为 Q ,即将上述算法中的 j = (j + 1) % m 改为: j = (j + Q) % m ,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
【例】 PDP-11 小型计算机中的汇编程序所用的符合表,就采用此方法来解决冲突,所用表长 m = 1321 ,选用 Q = 25 。
(3)随机探测
随机探测的基本思想是:
将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
2、拉链法
(1)拉链法解决冲突的方法
拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
【例】设有 m = 5 , H(K) = K mod 5 ,关键字值序例 5 , 21 , 17 , 9 , 15 , 36 , 41 , 24 ,按外链地址法所建立的哈希表如下图所示:
(2)拉链法的优点
与开放定址法相比,拉链法有如下几个优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
(3)拉链法的缺点
拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
========================
哈希法又称散列法、杂凑法以及关键字地址计算法等,相应的表称为哈希表。这种方法的基本思想是:首先在元素的关键字k和元素的存储位置p之间建立一个对应关系f,使得p=f(k),f称为哈希函数。创建哈希表时,把关键字为k的元素直接存入地址为f(k)的单元;以后当查找关键字为k的元素时,再利用哈希函数计算出该元素的存储位置p=f(k),从而达到按关键字直接存取元素的目的。
当关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上,即 k1≠k2 ,但 H(k1)=H(k2),这种现象称为冲突,此时称k1和k2为同义词。实际中,冲突是不可避免的,只能通过改进哈希函数的性能来减少冲突。
综上所述,哈希法主要包括以下两方面的内容:
1)如何构造哈希函数
2)如何处理冲突。
8.4.1 哈希函数的构造方法
构造哈希函数的原则是:①函数本身便于计算;②计算出来的地址分布均匀,即对任一关键字k,f(k) 对应不同地址的概率相等,目的是尽可能减少冲突。
下面介绍构造哈希函数常用的五种方法。
1. 数字分析法
如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。例如,有80个记录,关键字为8位十进制整数d1d2d3…d7d8,如哈希表长取100,则哈希表的地址空间为:00~99。假设经过分析,各关键字中 d4和d7的取值分布较均匀,则哈希函数为:h(key)=h(d1d2d3…d7d8)=d4d7。例如,h(81346532)=43,h(81301367)=06。相反,假设经过分析,各关键字中 d1和d8的取值分布极不均匀, d1 都等于5,d8 都等于2,此时,如果哈希函数为:h(key)=h(d1d2d3…d7d8)=d1d8,则所有关键字的地址码都是52,显然不可取。
2. 平方取中法
当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址,如图8.23所示。
关键字
内部编码
内部编码的平方值
H(k)关键字的哈希地址
KEYA
11050201
122157778355001
778
KYAB
11250102
126564795010404
795
AKEY
01110525
001233265775625
265
BKEY
02110525
004454315775625
315
图8.23平方取中法求得的哈希地址
3. 分段叠加法
这种方法是按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。具体方法有折叠法与移位法。移位法是将分割后的每部分低位对齐相加,折叠法是从一端向另一端沿分割界来回折叠(奇数段为正序,偶数段为倒序),然后将各段相加。例如:key=12360324711202065,哈希表长度为1000,则应把关键字分成3位一段,在此舍去最低的两位65,分别进行移位叠加和折叠叠加,求得哈希地址为105和907,如图8.24所示。
1 2 3 1 2 3
6 0 3 3 0 6
2 4 7 2 4 7
1 1 2 2 1 1
+) 0 2 0 +) 0 2 0
———————— —————————
1 1 0 5 9 0 7
(a)移位叠加 (b) 折叠叠加
图8.24 由叠加法求哈希地址
4. 除留余数法
假设哈希表长为m,p为小于等于m的最大素数,则哈希函数为
h(k)=k % p ,其中%为模p取余运算。
例如,已知待散列元素为(18,75,60,43,54,90,46),表长m=10,p=7,则有
h(18)=18 % 7=4 h(75)=75 % 7=5 h(60)=60 % 7=4
h(43)=43 % 7=1 h(54)=54 % 7=5 h(90)=90 % 7=6
h(46)=46 % 7=4
此时冲突较多。为减少冲突,可取较大的m值和p值,如m=p=13,结果如下:
h(18)=18 % 13=5 h(75)=75 % 13=10 h(60)=60 % 13=8
h(43)=43 % 13=4 h(54)=54 % 13=2 h(90)=90 % 13=12
h(46)=46 % 13=7
此时没有冲突,如图8.25所示。
0 1 2 3 4 5 6 7 8 9 10 11 12
图8.25 除留余数法求哈希地址
5. 伪随机数法
采用一个伪随机函数做哈希函数,即h(key)=random(key)。
在实际应用中,应根据具体情况,灵活采用不同的方法,并用实际数据测试它的性能,以便做出正确判定。通常应考虑以下五个因素 :
l 计算哈希函数所需时间 (简单)。
l 关键字的长度。
l 哈希表大小。
l 关键字分布情况。
l 记录查找频率
8.4.2 处理冲突的方法
通过构造性能良好的哈希函数,可以减少冲突,但一般不可能完全避免冲突,因此解决冲突是哈希法的另一个关键问题。创建哈希表和查找哈希表都会遇到冲突,两种情况下解决冲突的方法应该一致。下面以创建哈希表为例,说明解决冲突的方法。常用的解决冲突方法有以下四种:
1. 开放定址法
这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式:
Hi=(H(key)+di)% m i=1,2,…,n
其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种:
l 线性探测再散列
dii=1,2,3,…,m-1
这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
l 二次探测再散列
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
l 伪随机探测再散列
di=伪随机数序列。
具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。
例如,已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。如果用线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元,参图8.26 (a)。如果用二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元,参图8.26 (b)。如果用伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,……..,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元,参图8.26 (c)。
0 1 2 3 4 5 6 7 8 9 10
(a) 用线性探测再散列处理冲突
0 1 2 3 4 5 6 7 8 9 10
(b) 用二次探测再散列处理冲突
0 1 2 3 4 5 6 7 8 9 10
(c) 用伪随机探测再散列处理冲突
图8.26开放地址法处理冲突
从上述例子可以看出,线性探测再散列容易产生“二次聚集”,即在处理同义词的冲突时又导致非同义词的冲突。例如,当表中i, i+1 ,i+2三个单元已满时,下一个哈希地址为i, 或i+1 ,或i+2,或i+3的元素,都将填入i+3这同一个单元,而这四个元素并非同义词。线性探测再散列的优点是:只要哈希表不满,就一定能找到一个不冲突的哈希地址,而二次探测再散列和伪随机探测再散列则不一定。
2. 再哈希法
这种方法是同时构造多个不同的哈希函数:
Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
3. 链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
例如,已知一组关键字(32,40,36,53,16,46,71,27,42,24,49,64),哈希表长度为13,哈希函数为:H(key)= key % 13,则用链地址法处理冲突的结果如图8.27所示:
图8.27 链地址法处理冲突时的哈希表
本例的平均查找长度 ASL=(1*7+2*4+3*1)=1.5
4、建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
0 0