做分词组件时,有网友提出采用Hashtable 数据结构查找字符串效率较低,建议改为Dictionary,其理由是采用Hashtable 时Key值是object 会触发装箱和拆箱动作,一直对这种说法表示怀疑,因为我理解只有值类型和引用类型通过object 互转时才会发生装箱和查询,引用类型之间强制转换不应发生装箱和拆箱,而Dictionary 泛型实际上底层还是调用的Hashtable,所以效率怎么会比Hashtable 要高呢?今天决定对比较常用的4种数据结构做一个测试,以便以后做系统性能优化时做一个参考。
这四种数据结构分别是 Hashtable, Dictionary, SortedDictionary,SortedList。它们所代表的数据结构分别是 哈希表、哈希表、二叉索引树和二分查找。测试设计如下。以10万条数据为一组,按照每条记录的字符串长度不同分为3组,分别是16,128,1024,并且分别根据原数据是排序的还是不排序的两种情况进行插入和单值查找的测试。测试时间单位为毫秒。下面看测试结果
插入的测试结果 (所有结果都是插入10万次的总时间)
测试条件 | HashTable | Dictionary | SortedDictionary | SortedList |
字符串长度 16,未排序 | 14 | 21 | 896 | 8009 |
字符串长度 16,已排序 | 25 | 35 | 990 | 671 |
字符串长度 128,未排序 | 30 | 52 | 868 | 8415 |
字符串长度 128,已排序 | 43 | 67 | 1053 | 666 |
字符串长度 1024,未排序 | 132 | 262 | 1269 | 8159 |
字符串长度 1024,已排序 | 158 | 277 | 1036 | 684 |
查询的测试结果 (所有结果都是查询10万次的总时间)
测试条件 | HashTable | Dictionary | SortedDictionary | SortedList |
字符串长度 16,未排序 | 13 | 15 | 412 | 366 |
字符串长度 16,已排序 | 25 | 29 | 349 | 315 |
字符串长度 128,未排序 | 31 | 40 | 492 | 438 |
字符串长度 128,已排序 | 42 | 54 | 408 | 371 |
字符串长度 1024,未排序 | 130 | 202 | 934 | 894 |
字符串长度 1024,已排序 | 160 | 219 | 801 | 757 |
从测试结果上看,无论是插入和查询,Hashtable的效率是最高的 其次是Dictionary 这和我的预期基本是一致的。
采用哈希方式插入,时间主要消耗在对哈希冲突情况的处理上,但由于只是选择空闲的桶而没有另外进行内存分配,其消耗时间是有限的。这里需要说明的是在我的测试设计中已经将每种数据结构的初始值设置为10万。而Dictionary 由于在Hashtable基础上封装了一层,效率稍有下降。
采用二叉搜索树插入,时间消耗在对树的节点的维护以及查找上(因为每次插入前需要确定记录是否有重复,重复记录不插入)。
二分查找,由于底层是一个顺序数组,当顺序插入时,效率比二叉搜索树稍高,当随机插入时会要求不断调整元素在数组中的位置,这导致大量的大块内存的拷贝,所以这种情况下效率极低,而且随着元素个数的增加会成指数上升。
哈希查找的时间消耗主要还是在冲突情况的匹配上,哈希函数的计算一般是很快的。但这里有一点必须要注意到,就是.net string 类型的特殊性,由于相同的string 指向相同的地址,所以在string 进行两两比较时,只比较地址,而不是每个字符都去计算,这大大提高了比较的效率。
而其他两种方式则需要比较字符串的大小,而不是仅仅比较相等,这带来了非常大的开销。
结论
对以字符串为主键的单值查找的优先选择 Hashtable 或者 Dictionary. 个人觉得如果只注重效率的话, Hashtable 更好一些,特别是在字符串较长时其效率几乎比Dictionary 快将近一倍。
测试代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
namespace StringDictionaryPerformance
{
class Program
{
static Random _Rand = new Random();
static Hashtable _Hashtable;
static Dictionary<string, int> _Dictionary;
static SortedDictionary<string, int> _SortDictionary ;
static SortedList<string, int> _SortedList ;
static string GetRandString(int length)
{
StringBuilder str = new StringBuilder();
for (int i = 0; i < length; i++)
{
str.Append((char)_Rand.Next(32, 128));
}
return str.ToString();
}
static List<string> GetTestStrings(int length, int number)
{
List<string> retVal = new List<string>(number);
for (int i = 0; i < number; i++)
{
retVal.Add(GetRandString(length));
}
return retVal;
}
static void TestInsert(List<string> strings, bool sort)
{
if (sort)
{
strings.Sort();
}
Console.WriteLine(string.Format("TestInsert string length = {0} count of strings = {1} sort={2}",
strings[0].Length, strings.Count, sort));
Stopwatch stopWatch = new Stopwatch();
Console.WriteLine("Begin Hashtable");
_Hashtable = new Hashtable(strings.Count);
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < strings.Count; i++)
{
if (_Hashtable[strings[i]] != null)
{
_Hashtable.Add(strings[i], i);
}
}
stopWatch.Stop();
Console.WriteLine(string.Format("ElapsedMilliseconds = {0} ms", stopWatch.ElapsedMilliseconds));
Console.WriteLine("Begin Dictoinary");
_Dictionary = new Dictionary<string,int>(strings.Count);
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < strings.Count; i++)
{
if (!_Dictionary.ContainsKey(strings[i]))
{
_Dictionary.Add(strings[i], i);
}
}
stopWatch.Stop();
Console.WriteLine(string.Format("ElapsedMilliseconds = {0} ms", stopWatch.ElapsedMilliseconds));
Console.WriteLine("Begin SortDictionary");
_SortDictionary = new SortedDictionary<string, int>();
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < strings.Count; i++)
{
if (!_SortDictionary.ContainsKey(strings[i]))
{
_SortDictionary.Add(strings[i], i);
}
}
stopWatch.Stop();
Console.WriteLine(string.Format("ElapsedMilliseconds = {0} ms", stopWatch.ElapsedMilliseconds));
Console.WriteLine("Begin SortedList");
_SortedList = new SortedList<string, int>(strings.Count);
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < strings.Count; i++)
{
if (!_SortedList.ContainsKey(strings[i]))
{
_SortedList.Add(strings[i], i);
}
}
stopWatch.Stop();
Console.WriteLine(string.Format("ElapsedMilliseconds = {0} ms", stopWatch.ElapsedMilliseconds));
}
static void TestFind(List<string> strings, bool sort)
{
Console.WriteLine(string.Format("TestFind string length = {0} count of strings = {1} sort={2}",
strings[0].Length, strings.Count, sort));
Stopwatch stopWatch = new Stopwatch();
Console.WriteLine("Begin Hashtable");
_Hashtable = new Hashtable(strings.Count);
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < strings.Count; i++)
{
if (_Hashtable[strings[i]] != null)
{
Console.WriteLine("Error!");
}
}
stopWatch.Stop();
Console.WriteLine(string.Format("ElapsedMilliseconds = {0} ms", stopWatch.ElapsedMilliseconds));
Console.WriteLine("Begin Dictoinary");
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < strings.Count; i++)
{
if (!_Dictionary.ContainsKey(strings[i]))
{
Console.WriteLine("Error!");
}
}
stopWatch.Stop();
Console.WriteLine(string.Format("ElapsedMilliseconds = {0} ms", stopWatch.ElapsedMilliseconds));
Console.WriteLine("Begin SortDictionary");
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < strings.Count; i++)
{
if (!_SortDictionary.ContainsKey(strings[i]))
{
Console.WriteLine("Error!");
}
}
stopWatch.Stop();
Console.WriteLine(string.Format("ElapsedMilliseconds = {0} ms", stopWatch.ElapsedMilliseconds));
Console.WriteLine("Begin SortedList");
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < strings.Count; i++)
{
if (!_SortedList.ContainsKey(strings[i]))
{
Console.WriteLine("Error!");
}
}
stopWatch.Stop();
Console.WriteLine(string.Format("ElapsedMilliseconds = {0} ms", stopWatch.ElapsedMilliseconds));
}
static void Main(string[] args)
{
List<string> strings;
strings = GetTestStrings(16, 100000);
TestInsert(strings, false);
TestFind(strings, false);
TestInsert(strings, true);
TestFind(strings, true);
strings = GetTestStrings(128, 100000);
TestInsert(strings, false);
TestFind(strings, false);
TestInsert(strings, true);
TestFind(strings, true);
strings = GetTestStrings(1024, 100000);
TestInsert(strings, false);
TestFind(strings, false);
TestInsert(strings, true);
TestFind(strings, true);
}
}
}