HashTable和HashSet中的类型陷阱
HashTable和HashSet中的类型陷阱
发现这个陷阱的起因是这样的:我现在有上百万字符串,我准备用TopK算法统计出出现次数做多的前100个字符串。
首先我用Hashtable统计出了每个字符串出现的次数,
然后我突然发现需要用一个字典把这些字符串中无用的词过滤掉,所以我又定义了一个HashSet作为统计字典。
我最初的代码如下:

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万字符串)
可是当我运行以后,竟然很久都没有出现结果,终于控制台上输出了2400000,竟然运行了2400秒!
仔细想了以后,首先加载字典不可能消耗什么时间,唯一可能消耗时间的就是这段语句了

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,

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类型,编译器会自动调用第二种实现,如果不是,就会调用第一种
HashTable和HashSet中的类型陷阱的更多相关文章
- 集合Hashtable Dictionary Hashset
#region Dictionary<K,V> Dictionary<string, Person> dict = new Dictionary<string, Pers ...
- HashTable、HashSet和Dictionary的区别
今天又去面试了,结果依然很悲催,平时太过于关注表面上的东西,有些实质却不太清楚,遇到HashTable和Dictionary相关的知识,记录下来,希望对后来人有所帮助,以及对自己以后复习可以参考. 1 ...
- HashMap, HashTable,HashSet,TreeMap 的时间复杂度
hashSet,hashtable,hashMap 都是基于散列函数, 时间复杂度 O(1) 但是如果太差的话是O(n) TreeSet==>O(log(n))==> 基于树的搜索,只需要 ...
- HashTable、HashSet和Dictionary的区别(转载)
1.HashTable哈希表(HashTable)表示键/值对的集合.在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类 ...
- C#高级应用之------HashTable、HashSet和Dictionary的区别(转)
原文url:http://www.cnblogs.com/akwwl/p/3680376.html 今天又去面试了,结果依然很悲催,平时太过于关注表面上的东西,有些实质却不太清楚,遇到HashTabl ...
- JavaScript中的this陷阱的最全收集 没有之一
当有人问起你JavaScript有什么特点的时候,你可能立马就想到了单线程.事件驱动.面向对象等一堆词语,但是如果真的让你解释一下这些概 念,可能真解释不清楚.有句话这么说:如果你不能向一个6岁小孩解 ...
- 转:JavaScript中的this陷阱的最全收集
在其他地方看到的,觉得解释的狠详细,特此分享 当有人问起你JavaScript有什么特点的时候,你可能立马就想到了单线程.事件驱动.面向对象等一堆词语,但是如果真的让你解释一下这些概念,可能真解释不清 ...
- Java中泛型 类型擦除
转自:Java中泛型是类型擦除的 Java 泛型(Generic)的引入加强了参数类型的安全性,减少了类型的转换,但有一点需要注意:Java 的泛型在编译器有效,在运行期被删除,也就是说所有泛型参数类 ...
- Lua中的类型与值
[基础介绍] Lua是一种动态类型的语言.在语言中没有类型定义的语法,每个值都带有其自身的类型信息.在Lua中有8中基本类型,分别是: nil(空)类型 boolean(布尔)类型 number(数字 ...
随机推荐
- 【百度地图API】自行获取区域经纬度的工具
原文:[百度地图API]自行获取区域经纬度的工具 摘要:上一章教大家如何建立自己的行政区域地图.这次为大家提供一个,可视化选择区域,并且能自动生成经纬度代码的工具.工具的源代码完全公开,并且做了详尽的 ...
- Java数据结构与算法(5) - ch05链表(LinkList)
双端链表与传统链表非常相似,但是它有一个新增的特性:即对最后一个链节点的引用,就像对第一个连接点的引用一样.注意与双向链表进行区别.
- Gaea是支持跨平台具有高并发、高性能、高可靠性,并提供异步、多协议、事件驱动的中间层服务框架
Gaea是支持跨平台具有高并发.高性能.高可靠性,并提供异步.多协议.事件驱动的中间层服务框架 Gaea:58同城开源的中间层服务框架 https://github.com/58code/Gaea 中 ...
- webBrowser.execWB的完整说明
原文:webBrowser.execWB的完整说明 在不是js打开的页面上按window.close(), 会有提示框,很烦,现在可以不用了,没有提示框直接关闭窗口.试试下面代码: <objec ...
- iframe的各项參数
iframe的各项參数: <iframe src="test.jsp" width="100″ height="50″ frameborder=" ...
- 继续推荐几款VisualStudio的插件
原文:继续推荐几款VisualStudio的插件 继前几天推荐了一款转换vs插件的插件后,借着安装VS2013之际,把我比较喜欢的几个插件继续推荐一下. C# Outline 2013 2013 C# ...
- Windows socket之最简单的socket程序
原文:Windows socket之最简单的socket程序 最简单的服务器的socket程序流程如下(面向连接的TCP连接 ): 1. WSAStartup(); 初始化网络库的使用. 2. soc ...
- java设计模式之十桥接模式(Bridge)
桥接模式就是把事物和其具体实现分开,使他们可以各自独立的变化.桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的JDBC桥DriverManager一样,JDBC进行连接数据库的时 ...
- 【UVA】10285-Longest Run on a Snowboard(动态规划)
这是一个简单的问题.你并不需要打印路径. 状态方程dp[i][j] = max(dp[i-1][j],dp[i][j-1],dp[i+1][j],dp[i][j+1]); 14003395 10285 ...
- ORACLE经常使用的命令
一个.ORACLE启动和关机 1.在独立环境中 要启用或禁用ORACLE该系统必须切换到ORACLE用户,例如以下 su-oracle a.启动ORACLE系统 oracle>svrmgrl S ...