为啥字典比列表快得多?

Posted

技术标签:

【中文标题】为啥字典比列表快得多?【英文标题】:Why is dictionary so much faster than list?为什么字典比列表快得多? 【发布时间】:2013-06-03 09:10:47 【问题描述】:

我正在测试从 Dictionary VS 列表中获取数据的速度。 我用这段代码测试过:

    internal class Program

    private static void Main(string[] args)
    
        var stopwatch = new Stopwatch();
        List<Grade> grades = Grade.GetData().ToList();
        List<Student> students = Student.GetStudents().ToList();

        stopwatch.Start();
        foreach (Student student in students)
        
            student.Grade = grades.Single(x => x.StudentId == student.Id).Value;
        
        stopwatch.Stop();
        Console.WriteLine("Using list 0", stopwatch.Elapsed);
        stopwatch.Reset();
        students = Student.GetStudents().ToList();
        stopwatch.Start();
        Dictionary<Guid, string> dic = Grade.GetData().ToDictionary(x => x.StudentId, x => x.Value);
        foreach (Student student in students)
        
            student.Grade = dic[student.Id];
        
        stopwatch.Stop();
        Console.WriteLine("Using dictionary 0", stopwatch.Elapsed);
        Console.ReadKey();
    


public class GuidHelper

    public static List<Guid> ListOfIds=new List<Guid>();

    static GuidHelper()
    
        for (int i = 0; i < 10000; i++)
        
            ListOfIds.Add(Guid.NewGuid());
        
    



public class Grade

    public Guid StudentId  get; set; 
    public string Value  get; set; 

    public static IEnumerable<Grade> GetData()
    
        for (int i = 0; i < 10000; i++)
        
            yield return new Grade
                             
                                 StudentId = GuidHelper.ListOfIds[i], Value = "Value " + i
                             ;
        
    


public class Student

    public Guid Id  get; set; 
    public string Name  get; set; 
    public string Grade  get; set; 

    public static IEnumerable<Student> GetStudents()
    
        for (int i = 0; i < 10000; i++)
        
            yield return new Student
                             
                                 Id = GuidHelper.ListOfIds[i],
                                 Name = "Name " + i
                             ;
        
    

内存中有学生和成绩的列表,他们有共同的 StudentId。 在第一种方式中,我尝试在我的机器上花费近 7 秒的列表上使用 LINQ 查找学生的成绩,而在另一种方式中,我首先将 List 转换为字典,然后使用不到一秒的键从字典中查找学生的成绩。

【问题讨论】:

您是否尝试过相反的方法(先使用字典测试,然后再使用列表)? @Fendy 是的。没有区别。 它被称为Shlemiel the painter's algorithm。答案解释了为什么会这样。 了解一些简单的数据结构确实有助于编程,所以我建议您阅读一些相关信息。 更合理的比较是字典(键)与列表(索引)。 【参考方案1】:

当你这样做时:

student.Grade = grades.Single(x =&gt; x.StudentId == student.Id).Value;

正如所写,它必须枚举整个List,直到它在 List 中找到具有正确 studentId 的条目(条目 0 是否与 lambda 匹配?不...条目 1 是否与 lambda 匹配?不...等等等等)。这是 O(n)。因为你对每个学生都做一次,所以它是 O(n^2)。

但是当你这样做时:

student.Grade = dic[student.Id];

如果你想在字典中按键查找某个元素,它可以立即跳转到它在字典中的位置——这是 O(1)。 O(n) 为每个学生做这件事。 (如果您想知道这是如何完成的 - 字典对键运行数学运算,将其转换为字典内的某个位置的值,与插入时放置的位置相同)

所以,字典更快,因为你使用了更好的算法。

【讨论】:

另外,由于使用了Single,它会一直迭代到最后,以确保只存在一个元素(如果找到则抛出异常)。 这是描述平衡树的一种神秘方式,还是那些单独的算法?确定我是否混淆了事情,但 b-tree 意味着 O(log n) 而不是 O(1)。 在 Dictionary 中查找元素不是 O(1),平均情况可能是 O(log n)。 @TimB 一棵树是 O(log(n)),因为它与 log(n) 一样深,其中 n 是元素的数量。在哈希表中,您在表中查看一次,它就在那里。当然,如果它很拥挤,您可能必须检查第二个或第三个,但问题是您获得多少“未命中”不是 n 的函数,而是您为哈希表分配了多少空间的函数。 只是为了提供术语,据说哈希表的摊销成本为 O(1)。不完全是 O(1)。由于Patashu给的原因【参考方案2】:

原因是因为字典是查找,而列表是迭代。

字典使用哈希查找,而您的列表需要遍历列表,直到每次都从头到尾找到结果。

换一种说法。该列表将比第一项上的字典更快,因为没有什么可查找的。这是第一个项目,繁荣..它完成了。但第二次列表必须查看第一项,然后是第二项。第三次通过它必须查看第一项,然后是第二项,然后是第三项..等等..

因此每次迭代查找都需要越来越多的时间。列表越大,所需的时间就越长。虽然字典总是或多或少是固定的查找时间(它也会随着字典变大而增加,但速度要慢得多,因此相比之下它几乎是固定的)。

【讨论】:

【参考方案3】:

在使用 Dictionary 时,您使用 key 来检索您的信息,这使它能够更有效地找到它,而使用 List 您正在使用 Single Linq 表达式,因为它是一个列表,除了在整个列表中查找想要的项目之外别无选择。

【讨论】:

【参考方案4】:

字典使用散列来搜索数据。字典中的每个项目都存储在包含相同哈希的项目桶中。速度要快很多。

尝试对列表进行排序,这样会快一些。

【讨论】:

您必须说明如何对列表进行排序只有在您使用 List&lt;T&gt;.BinarySearch() 之类的东西时才会有所帮助;-)。 (list[list.BinarySearch(new Grade StudentId = student.Id, , Comparer&lt;Grade&gt;.Create((a, b) =&gt; a.StudentId.CompareTo(b.StudentId)))])【参考方案5】:

字典使用hash table,它是一个很棒的数据结构,因为它几乎可以立即将输入映射到相应的输出,正如已经指出的,它的复杂度为 O(1),这意味着或多或少的立即检索。

它的缺点是,为了提高性能,您需要提前大量空间(取决于实现,无论是单独的链接还是线性/二次探测,您可能至少需要与您计划存储的一样多,在后一种情况下可能加倍)并且您需要一个良好的散列算法,将您的输入("John Smith")唯一地映射到相应的输出,例如数组中的位置(hash_array[34521])。

按排序顺序列出条目也是一个问题。如果我可以引用***:

按特定顺序列出所有 n 个条目通常需要 单独的排序步骤,其成本与每个条目的 log(n) 成正比。

阅读linear probing 和separate chaining 了解更多细节:)

【讨论】:

【参考方案6】:

字典基于哈希表,这是一种查找事物的相当有效的算法。在列表中,您必须逐个元素地查找内容。

这都是数据组织的问题......

【讨论】:

【参考方案7】:

在查找数据时,键控集合总是比非键控集合快。这是因为无键集合必须枚举其元素才能找到您要查找的内容。在键控集合中,您可以直接通过键访问元素。

这些是比较列表和字典的好文章。

Here。还有这个one。

【讨论】:

数组和列表也是“键控集合”。索引是来自 [0, n> 的键。字典键对域的约束更宽松。 @emperor:所以,如果我知道索引,那么从列表中检索数据...与从字典中检索数据(按键)一样快? @mike 实际上它更快。要访问数组的第 105 个元素(列表在内部使用数组),CPU 只需将 (105 - 1) * element size 添加到第一个元素的地址即可获得它在内存中的位置。使用字典,它必须计算键的哈希码,从中创建“桶”索引(hash code % bucket count)并逐一检查桶中的元素以找到具有给定键的元素。字典如何将元素分配到存储桶会影响性能,但在平均情况下,它足以最大限度地减少相同的项目过多。 @emperor:感谢您的澄清,这就是我所期望的;你之前的评论让我有点困惑;o)【参考方案8】:

来自 MSDN - 字典提到接近 O(1),但我认为这取决于所涉及的类型。

Dictionary(TKey,TValue) 泛型类提供了从一组键到一组值的映射。字典中的每个添加都包含一个值及其关联的键。使用它的键检索一个值非常快,接近 O(1),因为 Dictionary 类是作为哈希表实现的。

注意: 检索速度取决于为 TKey 指定的类型的散列算法的质量。

List(TValue) 没有实现哈希查找,所以它是顺序的,性能是 O(n)。它还取决于所涉及的类型以及需要考虑装箱/拆箱。

【讨论】:

以上是关于为啥字典比列表快得多?的主要内容,如果未能解决你的问题,请参考以下文章

为啥性能测试显示我的代码列表比数组快得多? [复制]

为啥 KNN 比决策树快得多?

为啥 memcmp 比 for 循环检查快得多?

为啥 MySQL JOIN 比 WHERE IN (子查询) 快得多

为啥从大表中查询 COUNT() 比 SUM() 快得多

为啥用 C 复制文件比 C++ 快得多?