HashTable和HashSet中的類型陷阱

發現這個陷阱的原由是這樣的:我如今有上百萬字符串,我準備用TopK算法統計出出現次數作多的前100個字符串。算法

首先我用Hashtable統計出了每一個字符串出現的次數,優化

而後我忽然發現須要用一個字典把這些字符串中無用的詞過濾掉,因此我又定義了一個HashSet做爲統計字典。ui

我最初的代碼以下:spa

 1     Stopwatch st = new Stopwatch();//計時器
 2             Hashtable queryTable = TopK.GetHashtable();//得到HashTable
 3             HashSet<string> test = new HashSet<string>();
 4             string path = "dic.txt";
 5             if (File.Exists(path))
 6             {
 7 
 8                 using (StreamReader sr = new StreamReader(path, System.Text.Encoding.Default))
 9                 {
10                     string s = string.Empty;
11                     while (!string.IsNullOrEmpty(s = sr.ReadLine()))
12                     {
13                         test.Add(s);
14                     }
15                 }
16             }//建立過濾字典
17             Hashtable queryTable2 = new Hashtable();
18             List<string> teststring = new List<string>();
19             var aa = teststring[0];
20             foreach (var key in queryTable.Keys)//對Hashtable中的key進行過濾
21             {
22 
23                 if (!test.Contains(key))
24                 {
25                     queryTable2.Add(key, queryTable[key]);
26                 }
27              
28             }
29             st.Stop();
30             Console.WriteLine(st.ElapsedMilliseconds);
31             Console.Read();

一眼看上去,這段代碼並無什麼錯誤,(HashTable中有120多萬字符串,字典中有11萬字符串)pwa

但是當我運行之後,居然好久都沒有出現結果,終於控制檯上輸出了2400000,居然運行了2400秒!code

仔細想了之後,首先加載字典不可能消耗什麼時間,惟一可能消耗時間的就是這段語句了blog

1    foreach (var key in queryTable.Keys)//對Hashtable中的key進行過濾
2             {
3 
4                 if (!test.Contains(key))
5                 {
6                     queryTable2.Add(key, queryTable[key]);
7                 }
8              
9             }

test是HashSet類型,它的查找,也就是contains方法的時間複雜度應該是O(1)啊,不該該那麼長時間啊,難道是var 定義的key,裝箱/拆箱致使的?接口

而後我將var改爲了string,ci

1       foreach (string key in queryTable.Keys)//對Hashtable中的key進行過濾
2             {
3 
4                 if (!test.Contains(key))
5                 {
6                     queryTable2.Add(key, queryTable[key]);
7                 }
8              
9             }

結果僅僅15秒控制檯就輸出了運行結果:1537字符串

 

可MSDN上對var的定義是:

在方法範圍中聲明的變量能夠具備隱式類型 var 隱式類型的本地變量是強類型變量(就好像您已經聲明該類型同樣),但由編譯器肯定類型。

可我HashTable中的key添加的是字符串啊,而後我又找到了HashTable.add方法的原型,

 
1 public virtual void Add (
2     Object key,
3     Object value
4 )
 

真是坑啊,原來Hashtable在添加元素的時候,自動轉化成了object類型

爲了一探究竟,再用ILspy查看底層源代碼,

找到if (!test.Contains(key))這一句

修改前

 1     IL_00a4: ldloc.s CS$5$0001
 2                 IL_00a6: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
 3                 IL_00ab: stloc.s key
 4                 IL_00ad: nop
 5                 IL_00ae: ldloc.2
 6                 IL_00af: ldloc.s key
 7                 IL_00b1: call bool [System.Core]System.Linq.Enumerable::Contains<object>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, !!0)
 8                 IL_00b6: stloc.s CS$4$0000
 9                 IL_00b8: ldloc.s CS$4$0000
10                 IL_00ba: brtrue.s IL_00d0

因爲編譯器默認key爲object類型,它居然調用了IEnumerable接口的Contains方法的實現,mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, !!0)

(HashSet實現了IEnumerable)也就是不斷的去調用HashSet的每一個元素的Equals方法和key去比較。。。

怪不得運行了那麼長時間

修改後

 1             IL_00a4: ldloc.s CS$5$0001
 2                 IL_00a6: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
 3                 IL_00ab: castclass [mscorlib]System.String
 4                 IL_00b0: stloc.s key
 5                 IL_00b2: nop
 6                 IL_00b3: ldloc.2
 7                 IL_00b4: ldloc.s key
 8                 IL_00b6: callvirt instance bool class [System.Core]System.Collections.Generic.HashSet`1<string>::Contains(!0)
 9                 IL_00bb: stloc.s CS$4$0000
10                 IL_00bd: ldloc.s CS$4$0000
11                 IL_00bf: brtrue.s IL_00d5

這時才調用了正常的HashSet的Contains實現[System.Core]System.Collections.Generic.HashSet`1<string>::Contains(!0)

時間複雜度爲O(1)

仔細思考,這裏還有一個陷阱就是在調用HashSet.Contains(object a)有兩種實現,

第一種就是咱們平時所熟悉的,調用IEnumerator的接口,把每一個元素和參數a比較(調用Equals方法),判斷a是否在HashSet中

第二種是泛型實現HashSet.Contains<T>(T a),T是咱們再定義HashSet時指定的類型,這時候Contains纔會採用哈希表的形式去查找a

而咱們在使用時,若是不指定類型T  ,編譯器會自動進行一次優化,編譯器會判斷a是否爲T類型,

若是爲T類型,編譯器會自動調用第二種實現,若是不是,就會調用第一種

相關文章
相關標籤/搜索