List <T> .Contains()很慢?


90

谁能解释我为什么使用泛型 List.Contains()函数这么慢?

我有List<long>大约一百万个数字,并且代码会不断检查这些数字中是否有特定数字。

我尝试使用Dictionary<long, byte>Dictionary.ContainsKey()函数执行相同的操作,它比使用List快10-20倍。

当然,我并不是真的想为此使用词典,因为它不是那样使用的。

所以,在这里真正的问题是,有没有替代品List<T>.Contains(),但没有怪诞的Dictionary<K,V>.ContainsKey()


2
字典有什么问题?适用于像您这样的情况。
卡马里

4
@Kamarey:HashSet可能是一个更好的选择。
布赖恩·拉斯穆森

HashSet是我一直在寻找的东西。
DSent,2009年

Answers:


156

如果只是检查是否存在,HashSet<T>.NET 3.5是最好的选择-类似字典的性能,但没有键/值对-仅是值:

    HashSet<int> data = new HashSet<int>();
    for (int i = 0; i < 1000000; i++)
    {
        data.Add(rand.Next(50000000));
    }
    bool contains = data.Contains(1234567); // etc

30

List.Contains是一个O(n)操作。

Dictionary.ContainsKey是O(1)运算,因为它使用对象的哈希码作为键,这使您可以更快地进行搜索。

我认为拥有包含一百万个条目的列表不是一个好主意。我认为List类不是为此目的而设计的。:)

例如,是否可以将那些millon实体保存到RDBMS中,并在该数据库上执行查询?

如果不可能,那么我还是要使用字典。


13
我不认为包含一百万个项目的列表没有任何不当之处,只是您可能不想在其上继续运行线性搜索。
迪恩

同意,列表或具有这么多条目的数组没有问题。只是不要扫描值。
Michael Krauklis 2010年

8

我想我有答案!是的,列表(数组)上的Contains()确实是O(n),但是如果数组很短并且您正在使用值类型,那么它仍然应该非常快。但是,我使用CLR Profiler(可从Microsoft免费下载)发现,Contains()是对值进行装箱以便比较它们,这需要堆分配,这非常昂贵(缓慢)。[注意:这是.Net 2.0;其他.Net版本未经测试。]

这是完整的故事和解决方案。我们有一个名为“ VI”的枚举,并创建了一个名为“ ValueIdList”的类,该类是VI对象列表(数组)的抽象类型。最初的实现是在古老的.Net 1.1天内,它使用了封装的ArrayList。我们最近在http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx中发现了中,在值类型(例如我们的值类型)上,泛型列表(List <VI>)的性能要比ArrayList好得多。枚举VI),因为这些值不必装箱。是的,它确实有效...几乎。

CLR Profiler令人惊讶。这是分配图的一部分:

  • ValueIdList ::包含bool(VI)5.5MB(34.81%)
  • 通用列表::包含bool(<UNKNOWN>)5.5MB(34.81%)
  • Generic.ObjectEqualityComparer <T> :: Equals bool(<UNKNOWN> <UNKNOWN>)5.5MB(34.88%)
  • 价值观.VI 7.7MB(49.03%)

如您所见,Contains()令人惊讶地调用Generic.ObjectEqualityComparer.Equals(),这显然需要将VI值装箱,这需要昂贵的堆分配。奇怪的是,Microsoft会取消列表中的装箱,而只是为了进行这样的简单操作而再次要求装箱。

我们的解决方案是重新编写Contains()实现,在我们的案例中,由于我们已经封装了通用列表对象(_items),因此实现起来很容易。这是简单的代码:

public bool Contains(VI id) 
{
  return IndexOf(id) >= 0;
}

public int IndexOf(VI id) 
{ 
  int i, count;

  count = _items.Count;
  for (i = 0; i < count; i++)
    if (_items[i] == id)
      return i;
  return -1;
}

public bool Remove(VI id) 
{
  int i;

  i = IndexOf(id);
  if (i < 0)
    return false;
  _items.RemoveAt(i);

  return true;
}

现在可以在我们自己的IndexOf()版本中完成VI值的比较,该版本不需要装箱,而且速度非常快。经过简单的重写,我们的特定程序加速了20%。O(n)...没问题!只是避免浪费内存!


感谢您的提示,我自己也被拳击表现不佳所困扰。Contains对于我的用例而言,自定义实现速度更快。
Lea Hayes 2014年

5

字典并不是那么糟糕,因为字典中的键被设计为可以快速找到。为了在列表中找到一个数字,它需要遍历整个列表。

当然,只有当您的号码是唯一的并且没有顺序时,字典才起作用。

我认为HashSet<T>.NET 3.5中还有一个类,它也仅允许唯一元素。


Dictionary <Type,integer>也可以有效地存储非唯一对象-使用整数来计算重复项的数量。例如,您将列表{a,b,a}存储为{a = 2,b = 1}。当然,它确实失去了装饰。
MSalters,2009年


2

这并不完全是您问题的答案,但我有一个可以提高Contains()在集合上的性能的类。我将Queue子类化,并添加了Dictionary,该哈希表将哈希码映射到对象列表。该Dictionary.Contains()功能是O(1),而List.Contains()Queue.Contains()Stack.Contains()是为O(n)。

字典的值类型是一个队列,其中包含具有相同哈希码的对象。调用方可以提供实现IEqualityComparer的自定义类对象。您可以将此模式用于堆栈或列表。该代码只需要进行一些更改。

/// <summary>
/// This is a class that mimics a queue, except the Contains() operation is O(1) rather     than O(n) thanks to an internal dictionary.
/// The dictionary remembers the hashcodes of the items that have been enqueued and dequeued.
/// Hashcode collisions are stored in a queue to maintain FIFO order.
/// </summary>
/// <typeparam name="T"></typeparam>
private class HashQueue<T> : Queue<T>
{
    private readonly IEqualityComparer<T> _comp;
    public readonly Dictionary<int, Queue<T>> _hashes; //_hashes.Count doesn't always equal base.Count (due to collisions)

    public HashQueue(IEqualityComparer<T> comp = null) : base()
    {
        this._comp = comp;
        this._hashes = new Dictionary<int, Queue<T>>();
    }

    public HashQueue(int capacity, IEqualityComparer<T> comp = null) : base(capacity)
    {
        this._comp = comp;
        this._hashes = new Dictionary<int, Queue<T>>(capacity);
    }

    public HashQueue(IEnumerable<T> collection, IEqualityComparer<T> comp = null) :     base(collection)
    {
        this._comp = comp;

        this._hashes = new Dictionary<int, Queue<T>>(base.Count);
        foreach (var item in collection)
        {
            this.EnqueueDictionary(item);
        }
    }

    public new void Enqueue(T item)
    {
        base.Enqueue(item); //add to queue
        this.EnqueueDictionary(item);
    }

    private void EnqueueDictionary(T item)
    {
        int hash = this._comp == null ? item.GetHashCode() :     this._comp.GetHashCode(item);
        Queue<T> temp;
        if (!this._hashes.TryGetValue(hash, out temp))
        {
            temp = new Queue<T>();
            this._hashes.Add(hash, temp);
        }
        temp.Enqueue(item);
    }

    public new T Dequeue()
    {
        T result = base.Dequeue(); //remove from queue

        int hash = this._comp == null ? result.GetHashCode() : this._comp.GetHashCode(result);
        Queue<T> temp;
        if (this._hashes.TryGetValue(hash, out temp))
        {
            temp.Dequeue();
            if (temp.Count == 0)
                this._hashes.Remove(hash);
        }

        return result;
    }

    public new bool Contains(T item)
    { //This is O(1), whereas Queue.Contains is (n)
        int hash = this._comp == null ? item.GetHashCode() : this._comp.GetHashCode(item);
        return this._hashes.ContainsKey(hash);
    }

    public new void Clear()
    {
        foreach (var item in this._hashes.Values)
            item.Clear(); //clear collision lists

        this._hashes.Clear(); //clear dictionary

        base.Clear(); //clear queue
    }
}

我的简单测试表明,我的HashQueue.Contains()跑步速度比Queue.Contains()。对于HashQueue版本,运行计数设置为10,000的测试代码需要0.00045秒,而对于Queue版本则需要0.37秒。HashQueue版本的计数为100,000,花费0.0031秒,而Queue花费36.38秒!

这是我的测试代码:

static void Main(string[] args)
{
    int count = 10000;

    { //HashQueue
        var q = new HashQueue<int>(count);

        for (int i = 0; i < count; i++) //load queue (not timed)
            q.Enqueue(i);

        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            bool contains = q.Contains(i);
        }
        sw.Stop();
        Console.WriteLine(string.Format("HashQueue, {0}", sw.Elapsed));
    }

    { //Queue
        var q = new Queue<int>(count);

        for (int i = 0; i < count; i++) //load queue (not timed)
            q.Enqueue(i);

        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            bool contains = q.Contains(i);
        }
        sw.Stop();
        Console.WriteLine(string.Format("Queue,     {0}", sw.Elapsed));
    }

    Console.ReadLine();
}

我刚刚为HashSet <T>添加了第三个测试用例,它似乎比您的解决方案甚至获得更好的结果: HashQueue, 00:00:00.0004029 Queue, 00:00:00.3901439 HashSet, 00:00:00.0001716
psulek 2015年

1

为什么字典不合适?

要查看列表中是否有特定值,您需要遍历整个列表。使用字典(或其他基于哈希的容器),可以更快地缩小需要比较的对象的数量。密钥(在您的情况下为数字)是经过哈希处理的,它为字典提供了要比较的对象的分数子集。


0

我在不支持HashSet的Compact Framework中使用了它,我选择了Dictionary,其中两个字符串都是我要查找的值。

这意味着我可以获得具有字典性能的list <>功能。这有点hacky,但是可以用。


1
如果使用字典代替HashSet,则最好将值设置为“”,而不是与键相同的字符串。这样,您将使用更少的内存。另外,您甚至可以使用Dictionary <string,bool>并将它们全部设置为true(或false)。我不知道哪个会使用更少的内存,一个空字符串或布尔值。我的猜测将是布尔。
TTT 2012年

在字典中,对于32或64位系统,string引用和bool值相差3或7个字节。但是请注意,每个条目的大小分别四舍五入为4或8的倍数。因此,在string和之间进行选择bool可能不会对大小造成任何影响。空字符串""确实一直作为静态属性存在于内存中string.Empty,因此无论是否在字典中使用它,都没有任何区别。(无论如何,它都会在其他地方使用。)
Wormbo,2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.