散列是一種常見的儲存資料的技術,按照這種方式可以非常迅速地插入和取回資料。散列所採用的資料結構被稱為是散列表。儘管散列表提供了快速地插入、刪除、以及取回資料的操作,但是諸如尋找最大值或最小值這樣的尋找操作,散列表卻無法執行地非常快。對於這類操作,其他資料結構會更適合。.NET 架構庫提供了一種非常有用的處理散列表的類,即Hashtable 類。
A.散列。散列表資料結構是圍繞數組設計的。雖然可以稍後根據需要增加數組的大小,但是數組是由第0 號元素一直到一些預定義尺寸的元素組成的。儲存在數組內的每一個資料項目都是基於一些資料區塊的,這被稱為是關鍵字。為了把一個元素儲存到散列表內,利用所謂的散列函數把關鍵字映射到一個範圍從0 到散列表大小的數上。散列函數的理想目標是把自身單元內的每一個關鍵字都儲存到數組內。然而,由於可能的關鍵字是不限制數量的,而數組的大小又是有限的,所以散列函數比較現實的目標是把關鍵字儘可能平均地分布到數組的單元內。
1.選擇散列函數。選擇散列函數是依據所用關鍵字的資料類型。如果所用的關鍵字是整數,那麼最簡單的函數是返回關鍵字對數組大小模數的結果。但是有些情況不建議使用這種方法,比如關鍵字都是以0 結束,且數組的大小為10 的情況。這就是數組的大小必須始終為素數的原因之一。此外,如果關鍵字是隨機整數,那麼散列函數應該更均勻地分布關鍵字。下面的程式舉例說明了此散列函數的工作原理:
using System;class chapter12{ static void Main() { string[] names = new string[99]; string name; string[] someNames = new string[]{"David","Jennifer", "Donnie", "Mayo","Raymond","Bernica", "Mike", "Clayton", "Beata", "Michael"}; int hashVal; for (int i = 0; i < 10; i++) { name = someNames[i]; hashVal = SimpleHash(name, names); names[hashVal] = name; } ShowDistrib(names); } static int SimpleHash(string s, string[] arr) { int tot = 0; char[] cname; cname = s.ToCharArray(); for (int i = 0; i <= cname.GetUpperBound(0); i++) tot += (int)cname[i]; return tot % arr.GetUpperBound(0); } static void ShowDistrib(string[] arr) { for (int i = 0; i <= arr.GetUpperBound(0); i++) if (arr[i] != null) Console.WriteLine(i + " " + arr[i]); }}
2.尋找散列表中資料。為了在散列表中尋找資料,需要計算關鍵字的散列值,然後訪問數組中的對應元素。就是這樣簡單。下面是函數:
static bool InHash(string s, string[] arr){ int hval = BetterHash(s, arr); if (arr[hval] == s) return true; else return false;}
3.解決衝突。在處理散列表的時候,不可避免地會遇到這種情況,即計算出的關鍵字的散列值已經儲存了另外一個關鍵字。這就是所謂的衝突。在發生衝突的時候可以使用幾種技術。這些技術包括桶式散列法、開放定址法、和雙重散列法。
(1).桶式散列法。桶是一種儲存在散列表元素內的簡單資料結構,它可以儲存多個資料項目。在大多數實現中,這種資料結構就是一個數組,但是在這裡的實現中將會使用arraylist,它會允許不考慮運行超出範圍而且允許分配更多的空間。最後,這種方資料結構會使實現更加高效。為了插入一個資料項目,首先用散列函數來確定哪一個arraylist 用來儲存資料項目。然後查看此資料項目是否已經在arraylist 內。如果存在,就什麼也不做。如果不存在,就調用Add 方法把此資料項目添加到arraylist 內。為了從散列表中移除一個資料項目,還是先確定要移除的資料項目的散列值,並且轉到對應的arraylist。然後查看來確信該資料項目在arraylist
內。如果存在,就把它移除掉。如下:
public class BucketHash{ private const int SIZE = 101; ArrayList[] data; public BucketHash() { data = new ArrayList[SIZE]; for (int i = 0; i <= SIZE - 1; i++) data[i] = new ArrayList(4); } public int Hash(string s) { long tot = 0; char[] charray; charray = s.ToCharArray(); for (int i = 0; i <= s.Length - 1; i++) tot += 37 * tot + (int)charray[i]; tot = tot % data.GetUpperBound(0); if (tot < 0) tot += data.GetUpperBound(0); return (int)tot; } public void Insert(string item) { int hash_value; hash_value = Hash(itemvalue); if (data[hash_value].Contains(item)) data[hash_value].Add(item); } public void Remove(string item) { int hash_value; hash_value = Hash(item); if (data[hash_value].Contains(item)) data[hash_value].Remove(item); }}
當使用桶式散列法的時候,能做的最重要的事情就是保持所用的arraylist 元素的數量儘可能地少。在向散列表添加資料項目或從散列表移除資料項目的時候,這樣會最小化所需做的額外工作。在前面的代碼中,通過在構造器調用中設定每個arraylist 的初始容量就可以最小化arraylist 的大小。一旦有了衝突,arraylist 的容量會變為2,然後每次arraylist 滿時容量就會擴充兩倍。雖然用一個好的散列函數,arraylist 也不應該變得太大。散列表中元素數量與表大小的比率被稱為是負載係數。研究表明在負載係數為1.0
的時候,或者在表的大小恰好等於元素數量的時候,散列表的效能最佳。
(2).開放定址法。開放定址函數會在散列表數組內尋找空單元來放置資料項目。如果嘗試的第一個單元是滿的,那麼就嘗試下一個空單元,如此反覆直到最終找到一個空單元為止。大家在本小節內會看到兩種不同的開放定址策略:即線性探查和平方探查。線性探查法採用線性函數來確定試圖插入的數組單元。這就意味著會順次嘗試單元直到找到
一個空單元為止。線性探查的問題是數組內相鄰單元中的資料元素會趨近成聚類,從而使得後續空單元的探查變得更長久且效率更低。平方探查法解決了聚類問題。平方函數用來確定要嘗試哪個單元。
(3).雙重散列法。雙重散列法是一種有趣的衝突解決方案策略,但是實際上已經說明了平方探查法通常會獲得更好的效能。
B.HashTable類。Hashtable 類是Dictionary 對象的一種特殊類型,它儲存了索引值對,其中的數值都是在源於關鍵字的散列代碼的基礎上進行儲存的。這裡可以為關鍵字的資料類型指定散列函數或者使用內建的函數(稍後將討論它)。Hashtable 類是非常有效率的,而且應該把它用於任何可能自訂實現的地方。
1.Hashtable使用。Hashtable 類是System.Collections 命名空間的一部分內容,所以必須在程式開始部分匯入System.Collections。Hashtable 對象可執行個體化如下:
Hashtable symbols = new Hashtable();HashtTable symbols = new Hashtable(50);HashTtable symbols = new Hashtable(25, 3.0F);
利用Add 方法就可以把索引值對添加到散列表內。這個方法會取走兩個參數:即關鍵字和與關鍵字相關聯的數值。在計算完關鍵字的散列值之後,會把這個關鍵字添加到散列表內。如下:
Hashtable symbols = new Hashtable(25);symbols.Add(" salary", 100000);symbols.Add(" name", "David Durr");symbols.Add(" age", 43);symbols.Add(" dept", "Information Technology");
還可以用索引來給散列表添加元素,為了做到這樣,要編寫一條指派陳述式來把數值賦值給指定的關鍵字作為索引(這非常像數組的索引)。如果這個關鍵字已經不存在了,那麼就把一個新的散列元素添加到散列表內。如果這個關鍵字已經存在,那麼就用新的數值來覆蓋這個存在的數值。如下:
Symbols["sex"] = "Male";Symbols["age"];
Hashtable 類有兩個非常有用的方法用來從散列表中取回關鍵字和數值:即Keys 和Values。這些方法建立了一個Enumerator 對象,它允許使用For Each 迴圈或者其他一些技術來檢查關鍵字和數值。
using System;using System.Collections;class chapter12{ static void Main() { Hashtable symbols = new Hashtable(25); symbols.Add(" salary", 100000); symbols.Add(" name", "David Durr"); symbols.Add(" age", 45); symbols.Add(" dept", "Information Technology"); symbols["sex"] = "Male"; Console.WriteLine("The keys are: "); foreach (Object key in symbols.Keys) Console.WriteLine(key); Console.WriteLine(); Console.WriteLine("The values are: "); foreach (Object value in symbols.Values) Console.WriteLine(value); }}
2.Hashtable 類的實用方法。
(1).Count 屬性儲存區著散列表內元素的數量,它會返回一個整數。
(2).Clear 方法可以立刻從散列表中移除所有元素。
(3).Remove 方法會取走關鍵字,而且該方法會把指定關鍵字和相關聯的數值都移除。
(4).ContainsKey 方法查看該元素或者數值是否在散列表內。
3.Hashtable 的應用程式。程式首先從一個文字檔中讀入一系列術語和定義。這個過程是在子程式BuildGlossary 中編碼實現的。文字檔的結構是:單詞,定義,用逗號在單詞及其定義之間進行分隔。這個術語表中的每一個單詞都是單獨一個詞,但是術語表也可以很容易地替換處理短語。這就是用逗號而不用空格作分隔字元的原因。此外,這種結構允許使用單詞作為關鍵字,這是構造這個散列表的正確方法。另一個子程式DisplayWords 把單詞顯示在一個列表框內,所以使用者可以選取一個單詞來獲得它的定義。既然單詞就是關鍵字,所以能使用Keys
方法從散列表中正好返回單詞。然後,使用者就可以看到有定義的單詞了。使用者可以簡單地點擊列表框中的單詞來擷取其定義。用Item 方法就可以取回定義,並且把它顯示在文字框內。
using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Drawing;using System.Text;using System.Windows.Forms;using System.Collections;using System.IO ;namespace WindowsApplication3{ public partial class Form1 : Form { private Hashtable glossary = new Hashtable(); public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { BuildGlossary(glossary); DisplayWords(glossary); } private void BuildGlossary(Hashtable g) { StreamReader inFile; string line; string[] words; inFile = File.OpenText(@"c:\words.txt "); char[] delimiter = new char[] { ',' }; while (inFile.Peek() != -1) { line = inFile.ReadLine(); words = line.Split(delimiter); g.Add(words[0], words[1]); } inFile.Close(); } private void DisplayWords(Hashtable g) { Object[] words = new Object[100]; g.Keys.CopyTo(words, 0); for (int i = 0; i <= words.GetUpperBound(0); i++) if (!(words[i] == null)) lstWords.Items.Add((words[i])); } private void lstWords_SelectedIndexChanged(object sender, EventArgs e) { Object word; word = lstWords.SelectedItem; txtDefinition.Text = glossary[word].ToString(); } }}