发现这个陷阱的原由是这样的:我如今有上百万字符串,我准备用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类型,编译器会自动调用第二种实现,若是不是,就会调用第一种