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) ? //表長爲大於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; //h1
- incr = (uint)(1 + (((seed >> 5)+1) % ((uint)hashsize-1)));//h2
- return hashcode; //返回哈希碼
- }
- public virtual Object this[Object key] //索引器
- {
- get
- {
- uint seed; //h1
- uint incr; //h2
- uint hashcode = InitHash(key, buckets.Length,
- out seed, out incr);
- int ntry = 0; //用於表示h(key,i)中的i值
- bucket b;
- int bn = (int)(seed % (uint)buckets.Length); //h(key,0)
- do
- {
- b = buckets[bn];
- if (b.key == null) //b爲無衝突空位時
- { //找不到相應的鍵,返回空
- return null;
- }
- if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
- KeyEquals(b.key, key))
- { //查找成功
- return b.val;
- }
- bn = (int)(((long)bn + incr) %
- (uint)buckets.Length); //h(key+i)
- } 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; //h1
- uint incr = (uint)(1 + (((seed >> 5) + 1) %
- ((uint)newBuckets.Length - 1))); //h2
- 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)
- { //置hash_coll的高位爲1
- newBuckets[bn].hash_coll |=
- unchecked((int)0x80000000);
- }
- //二度哈希h1(key)+h2(key)
- 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)
- { //用於判斷兩key是否相等
- return item == null ? false : item.Equals(key);
- }
- //當add爲true時用作添加元素,當add爲false時用作修改元素值
- private void Insert(Object key, Object nvalue, bool add)
- { //如果超過允許存放元素個數的上限則擴容
- if (count >= loadsize)
- {
- expand();
- }
- uint seed; //h1
- uint incr; //h2
- uint hashcode = InitHash(key, buckets.Length,out seed, out incr);
- int ntry = 0; //用於表示h(key,i)中的i值
- 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;
- }
- //存在衝突則置hash_coll的最高位爲1
- 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; //h1
- uint incr; //h2
- uint hashcode = InitHash(key, buckets.Length,out seed, out incr);
- int ntry = 0; //h(key,i)中的i
- bucket b;
- int bn = (int)(seed % (uint)buckets.Length); //哈希地址
- do
- {
- b = buckets[bn];
- if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
- KeyEquals(b.key, key)) //如果找到相應的鍵值
- { //保留最高位,其餘清0
- buckets[bn].hash_coll &= unchecked((int)0x80000000);
- if (buckets[bn].hash_coll != 0) //如果原來存在衝突
- { //使key指向buckets
- buckets[bn].key = buckets;
- }
- else //原來不存在衝突
- { //置key爲空
- 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)
- { //不爲空位時打印索引、鍵、值、hash_coll
- 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
- { //是空位時則打印索引和hash_coll
- 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的實現有似的地方,比如兩者都是以數組爲基礎做進一步地抽象而來,兩者都可以成倍地自動擴展容量。
開放地址法 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])
- {
- //var code = HashtableHelper.GetStringHash(s);
- this.AddItem(
- this.keyArray[i],
- this.dataArray[i],
- newItems,
- this.keyArray[i],
- newKeys,
- this.flagArray);
- }
- }
- this.dataArray = newItems;
- this.keyArray = newKeys;
- this.flagArray = newFlags;
- // throw new NotImplementedException();
- }
-
- 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; //open addressing Xn=Xn-1 + b
- 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; //open addressing Xn=Xn-1 + b
- 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;
- //not exist
- }
- else if (keys[address] == hashCode)
- {
- if (!this.flagArray[address])
- {
- flags[address] = true;
- this.avaiableCapacity++;
- }
- }
- else
- {
- int nextAddress = address + this.l; //open addressing Xn=Xn-1 + b
- 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)
該方法的基本思想是:
探查過程終止於三種情況:
利用開放地址法的一般形式,線性探查法的探查序列爲:
用線性探測法處理衝突,思路清晰,算法簡單,但存在下列缺點:
① 處理溢出需另編程序。一般可另外設立一個溢出表,專門用來存放上述哈希表中放不下的記錄。此溢出表最簡單的結構是順序表,查找方法可用順序查找。
② 按上述算法建立起來的哈希表,刪除工作非常困難。假如要從哈希表 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 = 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),從而達到按關鍵字直接存取元素的目的。
綜上所述,哈希法主要包括以下兩方面的內容:
8.4.1 哈希函數的構造方法
下面介紹構造哈希函數常用的五種方法。
1.
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.
1
6
2
1
+)
(a)移位疊加
4.
假設哈希表長爲m,p爲小於等於m的最大素數,則哈希函數爲
h(k)=k
例如,已知待散列元素爲(18,75,60,43,54,90,46),表長m=10,p=7,則有
此時衝突較多。爲減少衝突,可取較大的m值和p值,如m=p=13,結果如下:
此時沒有衝突,如圖8.25所示。
0
|
|
54 |
|
43 |
18 |
|
46 |
60 |
|
75 |
|
90 |
5.
在實際應用中,應根據具體情況,靈活採用不同的方法,並用實際數據測試它的性能,以便做出正確判定。通常應考慮以下五個因素
l
l
l
l
l
8.4.2
1.
這種方法也稱再散列法,其基本思想是:當關鍵字key的哈希地址p=H(key)出現衝突時,以p爲基礎,產生另一個哈希地址p1,如果p1仍然衝突,再以p爲基礎,產生另一個哈希地址p2,…,直到找出一個不衝突的哈希地址pi