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

本文对比分析了.NET Framework中的三个泛型关键字查找类:Dictionary、SortedDictionary和SortedList的实现方式、功能特性和运行效率。通过理论分析与实验测试,为开发者选择合适的数据结构提供指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

近日在网上看到一篇介绍泛型集合类的文章,总结的比较经典,要作者网名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

 

小实验 :

CODE:

SortedDictionary <int , int > TestObject = new SortedDictionary <int , int >();

TestObject.Add(7, 2);

TestObject.Add(0, 1);

TestObject.Add(5, 3);

TestObject.Add(1, 1);

TestObject.Add(4, 4);

 

foreach (KeyValuePair <int , int > kvp in TestObject)

{

Console .WriteLine(kvp.Key);

}

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

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

 

另一种遍历方法 :

CODE:

SortedDictionary <int , int >.Enumerator sde = TestObject.GetEnumerator();

while (sde.MoveNext())

{

Console .WriteLine(sde.Current.Key);

}

 

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

QUOTE:

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

 

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

CODE:

Random ra = new Random ();

SortedList <int , int > TestObject = new SortedList <int , int >();

for (int i = 1; i <= 1000000; i++)

{

TestObject.Add(i, ra.Next());

}

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

 

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

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

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

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

 

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

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

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

 

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

 

2. 功能

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

Dictionary<TKey, TValue> 的功能 :

Add Clear ContainsKey ContainsValue Count (属性), Enumerator( 无序 ) Item( 属性 ) Remove

 

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

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

 

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

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

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

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

 

3. 速度

实践出真知 - 某名人。

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

 

我们的测试程序 :

CODE:

static class DictionarySpeedTest

{

static Random RandomGenerator = new Random ();

static List <Key_N_Data > ArrayListData = new List <Key_N_Data >();

static Dictionary <long , long > TestObject = new Dictionary <long , long >();

 

public struct Key_N_Data

{

public long Key;

public long Data;

}

 

const int ITEM_COUNT = 1000000;

const int TEST_COUNT = 500000;

 

static long LastTick;

 

public static void TimerStart(string Text)

{

Console .Write(Text);

LastTick = DateTime .Now.Ticks;

}

 

public static void TimerEnd()

{

long t = DateTime .Now.Ticks - LastTick;

Console .WriteLine(((t) / 10000).ToString() + " ms" );

}

 

public static void Main()

{

Process .GetCurrentProcess().PriorityClass = ProcessPriorityClass .High;

Console .WriteLine(TestObject.GetType().ToString());

 

TimerStart("Generating data... " );

for (int i = 1; i <= ITEM_COUNT; i++)

{

Key_N_Data ThisKeyData = default (Key_N_Data );

ThisKeyData.Key = ((long )RandomGenerator.Next() << 31) | RandomGenerator.Next();

ThisKeyData.Data = ((long )RandomGenerator.Next() << 31) | RandomGenerator.Next();

ArrayListData.Add(ThisKeyData);

}

TimerEnd();

 

TimerStart("Test 1: add data test... " );

foreach (Key_N_Data Item in ArrayListData)

{

TestObject.Add(Item.Key, Item.Data);

}

TimerEnd();

 

TimerStart("Test 2: find data test... " );

for (int i = 1; i <= TEST_COUNT; i++)

{

{

if (TestObject[ArrayListData[RandomGenerator.Next(0, ITEM_COUNT)].Key] != ArrayListData[RandomGenerator.Next(0, ITEM_COUNT)].Data)

Console .WriteLine("Error!" );

}

}

TimerEnd();

 

TimerStart("Test 3: remove data test..." );

for (int i = 1; i <= TEST_COUNT; i++)

{

TestObject.Remove(ArrayListData[RandomGenerator.Next(0, ITEM_COUNT)].Key);

}

TimerEnd();

 

Console .Read();

}

}

 

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

 

                  ADD    FIND   REMOVE

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

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

SortedList<TKey, TValue>       N/A

 

我们把 ITEM_COUNT TEST_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>(Hashtable BST) 还要快。这样说来, SortedList<TKey,TValue> 似乎又不是简单的数组了。 ( 不过我仍然觉得它没什么用 )

 

4. 小结

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

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

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

 

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

CODE:

class MyComparer : Comparer <long >

{

public override int Compare(long x, long y)

{

return Comparer <long >.Default.Compare(y, x);

}

}

使用:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值