• 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类型,编译器会自动调用第二种实现,如果不是,就会调用第一种

  • 相关阅读:
    awk书上练习
    矩阵运算
    从最大似然到EM算法浅解
    numpy 练习
    python lxml教程
    pycharm快捷键
    python正则表达式教程
    三门问题
    Solr本地服务器搭建及查询
    git简单使用
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3321897.html
Copyright © 2020-2023  润新知