字典表示一種複雜的數據結構,這種數據結構容許按照某個鍵來訪問元素。字典也稱爲映射或散列表。
字典的主要特性是能根據鍵快速查找值。也能夠自由添加和刪除元素,這有點像List<T>(http://www.cnblogs.com/afei-24/p/6824791.html),但沒有在內存中移動後續元素的性能開銷。
下圖是一個簡化表示,鍵會轉換位一個散列。利用散列建立一個數字,它將索引和值關聯起來。而後索引包含一個到值的連接。一個索引項能夠關聯多個值,索引能夠存儲爲一個樹型結構。
.NET Framework提供了幾個字典類。最主要的類是Dictionary<TKey,TValue>。
1.鍵的類型
用做字典中的鍵的類型必須重寫Object類的GetHashCode()方法。只要字典類須要肯定元素的位置,它就要調用GetHashCode()方法。GetHashCode()方法返回的int有字典用於計算在對應位置放置元素的索引。後面介紹這個算法,如今只須要知道它涉及素數,因此字典的容量是一個素數。
GetHashCode()方法的實現代碼必須知足的要求:
*相同的對象應老是返回相同的值
*不一樣的對象能夠返回相同的值
*它應執行的比較快,計算的開銷不大
*它不能拋出異常
*它應至少使用一個實例字段
*散列代碼值應平均分佈在int能夠存儲的這個數字範圍上
*散列代碼最好在對象的生存期中不發生變化
字典的性能取決於GetHashCode()方法的實現代碼。
散列代碼值應平均分佈在int能夠存儲的這個數字範圍上的緣由:
若是兩個鍵返回的散列代碼值會獲得相同的索引,字典類就必須尋找最近的可用空閒位置來存儲第二個數據項,這須要進行必定的搜索,以便之後檢索這一項。顯然這會下降性能,若是在排序的時候許多鍵都有相同的索引這中衝突會更可能出現。根據Microsoft的算法工做方式,當計算出來的散列代碼值平均分佈在int.MinValue和int.MaxValue之間時,這種風險會降到最低。
除了實現GetHashCode()方法以外,鍵類型還必須實現IEquatable<T>.Equals()方法,或重寫Object.Equals()方法。0由於不一樣的鍵對象可能返回相同的散列代碼,因此字典使用Equals()方法來比較鍵。字典檢查兩個鍵A和B是否相等,並調用A.Equals(B)方法。這表示必須確保下述條件老是成立:
若是A.Equals(B)返回true,則A.GetHashCode()和B.GetHashCode()老是返回相同的散列代碼。
這聽起來有點奇怪,但它很重要。若是上述條件不成立,這個字典還能工做,但會出現,把一個對象放在字典中後,就再也檢索不到它,或者返回了錯誤的項。
因此,若是爲Equals()方法提供了重寫版本,但沒提供GetHashCode()方法的重寫版本,C#編譯器會顯示一個警告。
對於System.Object,這個條件爲true,由於Equals()方法只是比較引用,GetHashCode()方法實際上返回一個僅基於對象地址的散列代碼。這說明,若是散列表基於一個鍵,而該鍵沒有重寫這些方法,這個散列表能夠工做,但只有對象徹底相同,鍵才被認爲是相等的。也就是說,把一個對象放在字典中時,必須將它與該鍵的引用關聯起來。也不能之後用相同的值實例化另外一個鍵對象。若是沒有重寫Equals()方法和GetHashCode()方法,在字典中使用類型時就不太方便。
System.String實現了IEquatable接口,並重載了GetHashCode()方法。Equals()方法提供了值的比較,GetHashCode()方法根據字符串的值返回一個散列代碼。所以,在字典中把字符串用在鍵很方便。
數字類型(如Int32)也實現了IEquatable接口,並重載了GetHashCode()方法。可是這些類型返回的散列代碼只能映射到值上。若是但願用做鍵的數字自己沒有分佈在可能的整數值範圍內,把整數用做鍵就不能知足鍵值的平均分佈規則,因而不能得到最佳的性能。Int32並不適合在字典中使用。
若是須要使用的鍵類型沒有實現IEquatable接口,並根據存儲在字典中的鍵值重載GetHashCode()方法,就能夠建立一個實現IEqualityComparer<T>接口的比較器。IEqualityComparer<T>接口定義了GetHashCode()方法和Equals()方法,並將傳遞的對象做爲參數,這樣能夠提供與對象類型不一樣的實現方式。
2.演示字典
建立一個員工ID(EmployeeId)結構,用做字典的鍵。存儲在字典中的數據是Employee類型的對象。
該結構的成員是表示員工的一個前綴字符和一個數字。這兩個變量都是隻讀的,只能在構造函數中初始化。字典中的鍵不該改變,這是必須保證的。
html
public struct EmployeeId : IEquatable<EmployeeId> { private readonly char prefix; private readonly int number; public EmployeeId(string id) { Contract.Requires<ArgumentNullException>(id != null); prefix = (id.ToUpper())[0]; int numLength = id.Length - 1; try { number = int.Parse(id.Substring(1, numLength > 6 ? 6 : numLength)); } catch (FormatException) { throw new Exception("Invalid EmployeeId format"); } } public override string ToString() { return prefix.ToString() + string.Format("{0,6:000000}", number); } //因爲沒有填滿整數取值範圍,GetHashCode方法將數字向左移動16位,再與原來的數字進行異或操做, //最後將結果乘以16進制數0x15051505。這樣,散列代碼在整數取值區域上的分佈就很均勻。 public override int GetHashCode() { return (number ^ number << 16) * 0x15051505; } public bool Equals(EmployeeId other) { if (other == null) return false; return (prefix == other.prefix && number == other.number); } //比較兩個EmployeeId對象的值 public override bool Equals(object obj) { return Equals((EmployeeId)obj); } public static bool operator ==(EmployeeId left, EmployeeId right) { return left.Equals(right); } public static bool operator !=(EmployeeId left, EmployeeId right) { return !(left == right); } } public class Employee { private string name; private decimal salary; private readonly EmployeeId id; public Employee(EmployeeId id, string name, decimal salary) { this.id = id; this.name = name; this.salary = salary; } public override string ToString() { return String.Format("{0}: {1, -20} {2:C}", id.ToString(), name, salary); } }
客戶端代碼: 算法
static void Main() { //構造函數指定了31個元素的容量。容量通常是素數。 //若是指定了一個不是素數的值,Dictionary<TKey,TValue>類會使用指定的整數後面緊接着的一個素數 var employees = new Dictionary<EmployeeId, Employee>(31); var idTony = new EmployeeId("C3755"); var tony = new Employee(idTony, "Tony Stewart", 379025.00m); employees.Add(idTony, tony); Console.WriteLine(tony); var idCarl = new EmployeeId("F3547"); var carl = new Employee(idCarl, "Carl Edwards", 403466.00m); employees.Add(idCarl, carl); Console.WriteLine(carl); var idKevin = new EmployeeId("C3386"); var kevin = new Employee(idKevin, "Kevin Harwick", 415261.00m); employees.Add(idKevin, kevin); Console.WriteLine(kevin); var idMatt = new EmployeeId("F3323"); var matt = new Employee(idMatt, "Matt Kenseth", 1589390.00m); employees[idMatt] = matt; Console.WriteLine(matt); var idBrad = new EmployeeId("D3234"); var brad = new Employee(idBrad, "Brad Keselowski", 322295.00m); employees[idBrad] = brad; Console.WriteLine(brad); }
3.Lookup類
Dictionary<TKey,TValue>類支持每一個鍵關聯一個值。Lookup<TKey,TElement>類把鍵映射到一個值集上。這個類在程序集System.Core中實現,用System.Linq定義。
Lookup<TKey,TElement>類不能像通常的字典那樣建立,必須調用ToLookup()方法,該方法返回一個Lookup<TKey,TElement>對象。ToLookup()方法是一個擴展方法,它能夠用於實現了IEnumerable<T>接口的全部類。
ToLookup()方法須要一個Func<TSource,Tkey>,Func<TSource,Tkey>定義了選擇器。 數據結構
static void Main() { var racers = new List<Racer>(); racers.Add(new Racer(26, "Jacques", "Villeneuve", "Canada", 11)); racers.Add(new Racer(18, "Alan", "Jones", "Australia", 12)); racers.Add(new Racer(11, "Jackie", "Stewart", "United Kingdom", 27)); racers.Add(new Racer(15, "James", "Hunt", "United Kingdom", 10)); racers.Add(new Racer(5, "Jack", "Brabham", "Australia", 14)); //國家相同的對象關聯到一個鍵 var lookupRacers = racers.ToLookup(r => r.Country); foreach (Racer r in lookupRacers["Australia"]) { Console.WriteLine(r); } }
輸出:
Alan Jones
Jack Brabham
4.有序字典
SortedDictionary<TKey,TValue>類是一個二叉搜索樹,其中的元素根據鍵來排序。該鍵類型必須實現IComparable<TKey>接口。
若是鍵的類型不能排序,還能夠建立一個實現了IComparer<TKey>接口的比較器,將比較器用做有序字典的構造函數的一個參數。
SortedDictionary<TKey,TValue>和有序列表SortedList<TKey,TValue>(http://www.cnblogs.com/afei-24/p/6830376.html)的區別:
*SortedList<TKey,TValue>類使用的內存比SortedDictionary<TKey,TValue>少。
*SortedDictionary<TKey,TValue>元素的插入和刪除操做比較快。
*在用已排序好的數據填充集合時,若不須要改變容量,ortedList<TKey,TValue>比較快。dom