發現這個陷阱的原由是這樣的:我如今有上百萬字符串,我準備用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類型,編譯器會自動調用第二種實現,若是不是,就會調用第一種