• .Net学习难点讨论系列8 泛型字典类比较


    近日在网上看到一篇介绍泛型集合类的文章,总结的比较经典,要作者网名iceboy,原文地址失效,可参见此处,原文使用VB.NET2005描述,我将其整理为C#版,转发在此处,留作学习之用。

    [] Dictionary<TKey,TValue>, SortedDictionary<TKey,TValue>, SortedList<TKey,TValue>横向评测

    Dictionary<TKey,TValue>SortedDictionary<TKey,TValue> SortedList<TKey,TValue>.NET Framework的三个泛型的关键字查找的类,都属于System.Collections.Generic命名空间。它们无论是名字还是功能都十分相似,以至于实际运用的时候我们会经常混淆。因此有必要比较一下它们。

    1. 实现

    查阅 MSDN 得到如下资料:

    Dictionary<TKey, TValue>泛型类提供了从一组键到一组值的映射。字典中的每个添加项都由一个值及其相关联的键组成。通过键来检索值的速度是非常快的,接近于O(1),这是因为Dictionary<TKey, TValue>类是作为一个哈希表来实现的。

    检索速度取决于为TKey指定的类型的哈希算法的质量。

    可见,Dictionary<TKey, TValue>基本上就是一个Hashtable。不过它比Hashtable类快,因为它支持泛型~(稍后我们会用实验证明,即使使用Object类型的Dictionary也比 Hashtable稍快)

    SortedDictionary<TKey, TValue>泛型类是检索运算复杂度为O(log n)的二叉搜索树,其中n是字典中的元素数。就这一点而言,它与SortedList<TKey, TValue>泛型类相似。这两个类具有相似的对象模型,并且都具有O(log n)的检索运算复杂度。这两个类的区别在于内存的使用以及插入和移除元素的速度。

    SortedList<TKey, TValue>使用的内存比SortedDictionary<TKey, TValue>少。

    SortedDictionary<TKey, TValue>可对未排序的数据执行更快的插入和移除操作:它的时间复杂度为O(log n),而SortedList<TKey, TValue>O(n)

    如果使用排序数据一次性填充列表,则SortedList<TKey, TValue> SortedDictionary<TKey, TValue>快。

    每个键/值对都可以作为KeyValuePair<TKey, TValue>结构进行检索,或作为 DictionaryEntry通过非泛型IDictionary接口进行检索。

    只要键用作SortedDictionary<TKey, TValue>中的键,它们就必须是不可变的。SortedDictionary<TKey, TValue>中的每个键必须是唯一的。键不能为 null引用),但是如果值类型TValue为引用类型,该值则可以为空。

    SortedDictionary<TKey, TValue>需要比较器实现来执行键比较。可以使用一个接受 comparer参数的构造函数来指定IComparer<T>泛型接口的实现;如果不指定实现,则使用默认的泛型比较器Comparer<T>。如果类型TKey实现IComparable<T>泛型接口,则默认比较器使用该实现。

    C#语言的foreach语句需要集合中每个元素的类型。由于SortedDictionary<TKey, TValue>的每个元素都是一个键/值对,因此元素类型既不是键的类型,也不是值的类型。而是KeyValuePair<TKey, TValue>类型。

    可见,SortedDictionary<TKey, TValue>类似一个平衡二叉查找树(AVL)。既然是 BST,我们当然可以对其进行中序遍历。有两种方法:

    1. foreach

    2. Object.GetEnumerator

    小实验:

     1 SortedDictionary<int, int> TestObject = new SortedDictionary<int, int>(); 
     2 TestObject.Add(7, 2); 
     3 TestObject.Add(0, 1); 
     4 TestObject.Add(5, 3); 
     5 TestObject.Add(1, 1); 
     6 TestObject.Add(4, 4); 
     7  
     8 foreach (KeyValuePair<int, int> kvp in TestObject) 
     9 { 
    10     Console.WriteLine(kvp.Key); 
    11 } 

    得到的顺序是0,1,4,5,7(SortedList<TKey, TValue>同样)

    但是如果把SortedDictionary<TKey, TValue>换成Dictionary<TKey, TValue>, 结果就是7,0,5,1,4

    另一种遍历方法

    1 SortedDictionary<int, int>.Enumerator sde = TestObject.GetEnumerator(); 
    2 while (sde.MoveNext()) 
    3 { 
    4     Console.WriteLine(sde.Current.Key); 
    5 } 

    SortedDictionary<TKey, TValue>类和SortedList<TKey, TValue>类之间的另一个区别是:SortedList<TKey, TValue>支持通过由KeysValues属性返回的集合对键和值执行高效的索引检索。访问此属性时无需重新生成列表,因为列表只是键和值的内部数组的包装。

    二叉树的插入操作怎么是 O(n)?

    网上有一种说法, 就是SortedList<TKey, TValue>内部就是两个数组, 插入的时候类似O(n^2)的插入排序(每个动作为O(n)),不过插入有序数据特别快(每个动作变成O(1))。同样的情况出现在删除数据。

    1 Random ra = new Random(); 
    2 SortedList<int, int> TestObject = new SortedList<int, int>(); 
    3 for (int i = 1; i <= 1000000; i++) 
    4 { 
    5     TestObject.Add(i, ra.Next()); 
    6 } 

    其中,ra.Next()用来生成随机数。

    上述代码执行速度相当快,因为插入的数据的Key值是有序的。

    如果把i换成1000000-i,则速度立刻慢得惨不忍睹。

    同样的情况出现在把i替换成随机数。在一段时间的等待后出错,因为Key值不能重复。

    这样说来,SortedList<TKey, TValue>不太像二叉树结构.

    SortedList<TKey, TValue>还有一个功能,就是直接访问Key值大小排名为kKey Value

    方法(使用属性)object.Key[k]object.Value[k)

    这更加印证了网上的说法.

    我认为SortedList没什么用 - 除非是对基本有序的数据,或者对内存非常吝啬。如果仅仅需要在BST上加上查找排名为k的节点的功能,可以使用一个经典算法:在每个节点上加上一个leftsize,储存它左子树的大小。

    2. 功能

    这三个类的功能上面都讲得差不多了。因为实现就决定了功能。这里小结一下。

    Dictionary<TKey, TValue>的功能:

    AddClearContainsKeyContainsValueCount(属性),Enumerator(无序)Item(属性) Remove

    SortedDictionary<TKey, TValue>新增的功能:

    Enumerator为有序 - 对应BST的中序遍历。

    SortedList<TKey, TValue>新增的功能:

    Capacity(属性) - 毕竟人家是数组

    IndexOfKeyIndexOfValue(返回Value对应Key的排名而不是 Value 的排名)

    Keys[k]Values[k] - 返回按照Key排序的数组的第k个元素

    3. 速度

    实践出真知 - 某名人。

    理论和实践不符就是错的 - Thity

    我们的测试程序

     1 static class DictionarySpeedTest 
     2 { 
     3     static Random RandomGenerator = new Random(); 
     4     static List<Key_N_Data> ArrayListData = new List<Key_N_Data>(); 
     5     static Dictionary<long, long> TestObject = new Dictionary<long, long>(); 
     6  
     7     public struct Key_N_Data 
     8     { 
     9         public long Key; 
    10         public long Data; 
    11     } 
    12      
    13     const int ITEM_COUNT = 1000000; 
    14     const int TEST_COUNT = 500000; 
    15  
    16     static long LastTick; 
    17  
    18     public static void TimerStart(string Text) 
    19     { 
    20         Console.Write(Text); 
    21         LastTick = DateTime.Now.Ticks; 
    22     } 
    23  
    24     public static void TimerEnd() 
    25     { 
    26         long t = DateTime.Now.Ticks - LastTick; 
    27         Console.WriteLine(((t) / 10000).ToString() + " ms"); 
    28     } 
    29  
    30 public static void Main() 
    31 { 
    32     Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; 
    33     Console.WriteLine(TestObject.GetType().ToString()); 
    34  
    35     TimerStart("Generating data... "); 
    36     for (int i = 1; i <= ITEM_COUNT; i++) 
    37     { 
    38         Key_N_Data ThisKeyData = default(Key_N_Data); 
    39         ThisKeyData.Key = ((long)RandomGenerator.Next() << 31) | RandomGenerator.Next(); 
    40         ThisKeyData.Data = ((long)RandomGenerator.Next() << 31) | RandomGenerator.Next(); 
    41         ArrayListData.Add(ThisKeyData); 
    42     } 
    43     TimerEnd(); 
    44  
    45     TimerStart("Test 1: add data test... "); 
    46     foreach (Key_N_Data Item in ArrayListData) 
    47     { 
    48         TestObject.Add(Item.Key, Item.Data); 
    49     } 
    50     TimerEnd(); 
    51  
    52     TimerStart("Test 2: find data test... "); 
    53     for (int i = 1; i <= TEST_COUNT; i++) 
    54     { 
    55         { 
    56             if (TestObject[ArrayListData[RandomGenerator.Next(0, ITEM_COUNT)].Key] != ArrayListData[RandomGenerator.Next(0, ITEM_COUNT)].Data) 
    57                 Console.WriteLine("Error!"); 
    58         } 
    59     } 
    60     TimerEnd(); 
    61  
    62     TimerStart("Test 3: remove data test..."); 
    63     for (int i = 1; i <= TEST_COUNT; i++) 
    64     { 
    65         TestObject.Remove(ArrayListData[RandomGenerator.Next(0, ITEM_COUNT)].Key); 
    66     } 
    67     TimerEnd(); 
    68  
    69     Console.Read(); 
    70     } 
    71 } 
    72     

    通过更改TestObject的类型,我们可以很方便地比较这三个类的速度。测试结果:

                      ADD    FIND   REMOVE

    Dictionary<TKey, TValue>       265ms  203ms  187ms

    SortedDictionary<TKey, TValue> 1843ms 828ms  1234ms

    SortedList<TKey, TValue>       N/A

    我们把ITEM_COUNTTEST_COUNT都减小10倍:

                      ADD    FIND   REMOVE

    Dictionary<TKey,TValue>       15ms   31ms   15ms

    SortedDictionary<TKey,TValue> 93ms   46ms   38ms

    SortedList<TKey,TValue>       8031ms 15ms   6046ms

    SortedList<TKey,TValue>的随机查找居然比Dictionary<TKey,TValue>SortedDictionary<TKey,TValue>(HashtableBST)还要快。这样说来,SortedList<TKey,TValue>似乎又不是简单的数组了。(不过我仍然觉得它没什么用)

    4. 小结

    如果只是当作索引使用,请用Dictionary<TKey,TValue>

    如果需要查找最小的几个元素,或者需要按顺序遍历元素,就用SortedDictionary<TKey,TValue>

    如果输入/删除的元素是基本增序的,或者访问次数远多于修改次数,或者需要访问第k大的元素,或者对内存吝啬得BT的情况,用SortedList<TKey,TValue>吧。(它居然成使用情况最多的了... orz)

    PS: 微软似乎也很吝啬,SortedDictionary<TKey,TValue>居然只支持增序(默认的比较器),如果要降序的话,我们得自己写一个比较器。

    1 class MyComparer : Comparer<long> 
    2 { 
    3     public override int Compare(long x, long y) 
    4     { 
    5         return Comparer<long>.Default.Compare(y, x); 
    6     } 
    7 } 

    使用:

    1 SortedList<long, long> TestObject = new SortedList<long, long>(new MyComparer());

    现在我们可以来进行一下刚开始的时候提到的Dictionary<TKey,TValue>(泛型)vs Hashtable(非泛型)对决。

    结果:

    ADD   FIND  REMOVE

    Dictionary(Of Long, Long)     271ms 203ms 187ms

    Dictionary(Of Object, Object) 468ms 312ms 234ms

    Hashtable                         859ms 390ms 218ms

    结论: 最好用Dictionary代替Hashtable

  • 相关阅读:
    flowableU应用
    公募REITs打新基本
    A股市场人气排名因子
    flowableUI 流程演示
    flowable之监听器分配
    flowable基础二,
    【python股票量化】人气指标AR策略编程实现
    Java使用hutool工具类发送http请求
    一个模拟消息订阅和发布的简单程序
    CAXA基本操作旋转
  • 原文地址:https://www.cnblogs.com/lsxqw2004/p/1381339.html
Copyright © 2020-2023  润新知