List和Dictionary泛型类查找效率浅析

List和Dictionary泛型类查找效率存在巨大差异,前段时间亲历了一次。事情的背景是开发一个匹配程序,将书籍(BookID)推荐给网友(UserID),生成今日推荐数据时,有条规则是同一书籍七日内不能推荐给同一网友。

同一书籍七日内不能推荐给同一网友规则的实现是程序不断优化的过程,第一版程序是直接取数据库,根据BookID+UserID查询七日内有无记录,有的话不进行分配。但随着数据量的增大,程序运行时间越来越长,于是开始优化。第一次优化是把所有七日内的数据取出来,放到List<T>中,然后再内存中进行查找,发现这样效率只是稍有提高,但不明显。第二次优化采用了Dictionary<TKey, TValue>,意外的发现效果不是一般的好,程序效率提高了几倍。

下面是伪代码,简化了程序代码,只是为说明List和Dictionary效率的差别,并不具备实际意义。

    /// <summary>
    /// 集合类效率测试
    /// </summary>
    public class SetEfficiencyTest
    {
        static List<TestModel> todayList = InitTodayData();
        static List<TestModel> historyList = InitHisoryData();

        public static void Run()
        {
            CodeTimer.Time("ListTest", 1, ListTest);
            CodeTimer.Time("DictionaryTest", 1, DictionaryTest);
        }

        public static void ListTest()
        {
            List<TestModel> resultList = todayList.FindAll(re =>
             {
                 if (historyList.Exists(m => m.UserID == re.UserID && m.BookID == re.BookID))
                 {
                     return false;
                 }
                 return true;
             });
        }

        public static void DictionaryTest()
        {
            Dictionary<int, List<string>> bDic = new Dictionary<int, List<string>>();
            foreach (TestModel obj in historyList)
            {
                if (!bDic.ContainsKey(obj.UserID))
                {
                    bDic.Add(obj.UserID, new List<string>());
                }
                bDic[obj.UserID].Add(obj.BookID);
            }

            List<TestModel> resultList = todayList.FindAll(re =>
            {
                if (bDic.ContainsKey(re.UserID) && bDic[re.UserID].Contains(re.BookID))
                {
                    return false;
                }
                return true;
            });
        }

        /// <summary>
        /// 初始化数据(今日)
        /// </summary>
        /// <returns></returns>
        public static List<TestModel> InitTodayData()
        {
            List<TestModel> list = new List<TestModel>();
            for (int i = 0; i < 10000; i++)
            {
                list.Add(new TestModel() { UserID = i, BookID = i.ToString() });
            }
            return list;
        }

        /// <summary>
        /// 初始化数据(历史)
        /// </summary>
        /// <returns></returns>
        public static List<TestModel> InitHisoryData()
        {
            List<TestModel> list = new List<TestModel>();
            Random r = new Random();
            int loopTimes = 60000;
            for (int i = 0; i < loopTimes; i++)
            {
                list.Add(new TestModel() { UserID = r.Next(0, loopTimes), BookID = i.ToString() });
            }
            return list;
        }

        /// <summary>
        /// 测试实体
        /// </summary>
        public class TestModel
        {
            /// <summary>
            /// 用户ID
            /// </summary>
            public int UserID { get; set; }

            /// <summary>
            /// 书ID
            /// </summary>
            public string BookID { get; set; }
        }
    }

输出如下:

真是想不到,两者效率相差这么多。接下来研究下两者差异巨大的原因。

List<T>.Exists()函数的实现:

        public bool Exists(Predicate<T> match)
        {
            return this.FindIndex(match) != -1;
        }

        public int FindIndex(Predicate<T> match)
        {
            return this.FindIndex(0, this._size, match);
        }
        public int FindIndex(int startIndex, int count, Predicate<T> match)
        {
            if (startIndex > this._size)
            {
                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_Index);
            }
            if (count < 0 || startIndex > this._size - count)
            {
                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_Count);
            }
            if (match == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
            }
            int num = startIndex + count;
            for (int i = startIndex; i < num; i++)
            {
                if (match(this._items[i]))
                {
                    return i;
                }
            }
            return -1;
        }

List<T>.Exists 本质是通过循环查找出该条数据,每一次的调用都会重头循环,所以效率很低。显然,这是不可取的。

Dictionary<TKey, TValue>.ContainsKey()函数的实现:

        public bool ContainsKey(TKey key)
        {
            return this.FindEntry(key) >= 0;
        }

        // System.Collections.Generic.Dictionary<TKey, TValue>
        private int FindEntry(TKey key)
        {
            if (key == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }
            if (this.buckets != null)
            {
                int num = this.comparer.GetHashCode(key) & 2147483647;
                for (int i = this.buckets[num % this.buckets.Length]; i >= 0; i = this.entries[i].next)
                {
                    if (this.entries[i].hashCode == num && this.comparer.Equals(this.entries[i].key, key))
                    {
                        return i;
                    }
                }
            }
            return -1;
        }

Dictionary<TKey, TValue>.ContainsKey() 内部是通过Hash查找实现的,所以效率比List高出很多。

最后,给出MSDN上的建议:

1.如果需要非常快地添加、删除和查找项目,而且不关心集合中项目的顺序,那么首先应该考虑使用 System.Collections.Generic.Dictionary<TKey, TValue>(或者您正在使用 .NET Framework 1.x,可以考虑 Hashtable)。三个基本操作(添加、删除和包含)都可快速操作,即使集合包含上百万的项目。

2.如果您的使用模式很少需要删除和大量添加,而重要的是保持集合的顺序,那么您仍然可以选择 List<T>。虽然查找速度可能比较慢(因为在搜索目标项目时需要遍历基础数组),但可以保证集合会保持特定的顺序。

3.您可以选择 Queue<T> 实现先进先出 (FIFO) 顺序或 Stack<T> 实现后进先出 (LIFO) 顺序。虽然 Queue<T> 和 Stack<T> 都支持枚举集合中的所有项目,但前者只支持在末尾插入和从开头删除,而后者只支持从开头插入和删除。

4.如果需要在实现快速插入的同时保持顺序,那么使用新的 LinkedList<T> 集合可帮助您提高性能。与 List<T> 不同,LinkedList<T> 是作为动态分配的对象链实现。与 List<T> 相比,在集合中间插入对象只需要更新两个连接和添加新项目。从性能的角度来看,链接列表的缺点是垃圾收集器会增加其活动,因为它必须遍历整个列表以确保没有对象没有被释放。另外,由于每个节点相关的开销以及每个节点在内存中的位置等原因,大的链接列表可能会出现性能问题。虽然将项目插入到 LinkedList<T> 的实际操作比在 List<T> 中插入要快得多,但是找到要插入新值的特定位置仍需遍历列表并找到正确的位置。

参考资料:CLR 完全介绍: 最佳实践集合, List和hashtable之查找效率

时间: 2024-10-21 23:30:26

List和Dictionary泛型类查找效率浅析的相关文章

Hashtable Dictionary List 谁效率更高

一 前言 很少接触HashTable晚上回来简单看了看,然后做一些增加和移除的操作,就想和List 与 Dictionary比较下存数据与取数据的差距,然后便有了如下的一此测试, 当然我测的方法可能不是很科学,但至少是我现在觉得比较靠谱的方法.如果朋友们有什么好的方法,欢迎提出大家来交流下. 先来简单介绍这三个容器的各自特点吧  1 hashtable 散列表(也叫哈希表),是根据关键字(Key value)而直接访问在内存存储位置的数据结构. 2 List<T> 是针对特定类型.任意长度的一

python 字典有序无序及查找效率,hash表

刚学python的时候认为字典是无序,通过多次插入,如di = {}, 多次di['testkey']='testvalue' 这样测试来证明无序的.后来接触到了字典查找效率这个东西,查了一下,原来字典在python内部是通过哈希表的顺序来排的,做了一些测试,比如di = {1:1,3:3,2:2,4:4,5:5} ,无论怎么改变键值对的顺序,print di 总是会{1: 1, 2: 2, 3: 3, 4: 4, 5: 5}.所以看起来当插入di['key']='value'时,这组键值对有时

如何在mysql查找效率慢的SQL语句

如何在mysql查找效率慢的SQL语句呢?这可能是困然很多人的一个问题,MySQL通过慢查询日志定位那些执行效率较低的SQL 语句,用--log-slow-queries[=file_name]选项启动时,mysqld 会写一个包含所有执行时间超过long_query_time 秒的SQL语句的日志文件,通过查看这个日志文件定位效率较低的SQL .下面介绍MySQL中如何查询慢的SQL语句 一.MySQL数据库有几个配置选项可以帮助我们及时捕获低效SQL语句 1,slow_query_log 这

下拉列表框DropDownList绑定Dictionary泛型类

DropDownList绑定Dictionary泛型类 定义一个Dictionary泛型类 /// <summary>    /// 产品类型    /// </summary>    /// <returns></returns>    public Dictionary<string, string> productType()    {        Dictionary<string, string> d = new Dict

python中in在list和dict中查找效率比较

转载自:http://blog.csdn.net/wzgbm/article/details/54691615 首先给一个简单的例子,测测list和dict查找的时间: import time query_lst = [-60000,-6000,-600,-60,-6,0,6,60,600,6000,60000] lst = [] dic = {} for i in range(100000000): lst.append(i) dic[i] = 1 start = time.time() fo

Python中字符串查找效率比较

Python中字符串查找方式有多种,常见的有re.match/search or str.find 用一个例子来说明各种方式的效率如下: from timeit import timeit import re def find(string, text): if string.find(text) > -1: pass def re_find(string, text): if re.match(text, string): pass def best_find(string, text): i

有序数组在数据量较少时候的查找效率比较

笔者曾经参加过某浏览器开发,记得当时在做浏览器放大和缩小的时候,产品经理规定滚动鼠标增加时百分之5,10,15,35,45,50,65,75,85,90,95,100,105,125,150,175,200.当时参加开发的同学就将这组数据做成一个表,然后每次滚动放大或者缩小都首先获取当前数值,然后从这个表中查需要改变多少再设置页面显示.不过查取的时候那位同学采用折半查找方法,认为这样效率很高. 可是事实真的这样吗? 总所周知,对于有序数组的折半查找可以将时间复杂度从顺序查找的N降低到lgN(以2

kafka解决查找效率的两大法宝

数据文件的分段 Kafka解决查询效率的手段之一是将数据文件分段,比如有100条Message,它们的offset是从0到99.假设将数据文件分成5段,第一段为0-19,第二段为20-39,以此类推,每段放在一个单独的数据文件里面,数据文件以该段中最小的offset命名.这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中. 为数据文件建索引 数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能

QVector与QMap查找效率实战(QMap快N倍)

因为项目使用QVector,太慢了,听说QMap比QVector查找时快,所以写一个小程序试试: 从30000个数据中找5000个 程序运行截图如下: QVector QMap 一样的数据,找一样的数代码如下图: widget.h #ifndef WIDGET_H#define WIDGET_H #include <QWidget>#include <QVector>#include <QMap> namespace Ui {class Widget;} typedef