Список<t>.Contains() очень медленный?

Может кто-нибудь объяснить мне, почему функция generics List.Contains() такая медленная?

У меня List<long> около миллиона номеров, и код, который постоянно проверяет, есть ли конкретное число в этих числах.

Я попытался сделать то же самое, используя Dictionary<long, byte> и функцию Dictionary.ContainsKey(), и это было примерно в 10-20 раз быстрее, чем со списком.

Конечно, я не хочу использовать словарь для этой цели, потому что он не предназначен для этого.

Таким образом, реальный вопрос здесь заключается в том, есть ли альтернатива List<T>.Contains(), но не такая странная, как Dictionary<K,V>.ContainsKey()?

Ответ 1

Если вы просто проверяете существование, 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

Ответ 2

List.Contains - это операция O (n).

Dictionary.ContainsKey - это операция O (1), поскольку она использует хэш-код объектов в качестве ключа, что дает вам более быструю возможность поиска.

Я не думаю, что неплохо иметь Список, содержащий миллион записей. Я не думаю, что класс List был разработан для этой цели.:)

Возможно ли сохранить эти миллионные объекты в RDBMS, например, и выполнить запросы в этой базе данных?

Если это невозможно, я все равно буду использовать словарь.

Ответ 3

Думаю, у меня есть ответ! Да, верно, что 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), потому что значения don ' t должен быть помещен в коробку. Это правда, и это сработало... почти.

Профайлер CLR показал удивление. Здесь часть графика распределения:

  • ValueIdList:: Содержит bool (VI) 5.5MB (34.81%)
  • Generic.List:: Содержит bool (<UNKNOWN> ) 5.5MB (34.81%)
  • Generic.ObjectEqualityComparer <T> :: Equals bool (<UNKNOWN> <UNKNOWN> ) 5,5 МБ (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;
}

Сравнение значений VI теперь выполняется в нашей собственной версии IndexOf(), которая не требует бокса, и это очень быстро. Наша специальная программа ускорилась на 20% после этой простой перезаписи. O (n)... не проблема! Просто избегайте использования использованной памяти!

Ответ 4

Словарь не так уж плох, потому что ключи в словаре предназначены для быстрого поиска. Чтобы найти номер в списке, ему необходимо выполнить итерацию по всему списку.

Конечно, словарь работает только в том случае, если ваши номера уникальны и не упорядочены.

Я думаю, что в .NET 3.5 есть класс HashSet<T>, он также позволяет использовать только уникальные элементы.

Ответ 5

A SortedList будет быстрее искать (но медленнее вставлять элементы)

Ответ 6

Это не совсем ответ на ваш вопрос, но у меня есть класс, который увеличивает производительность Contains() в коллекции. Я подклассифицировал очередь и добавил словарь, который отображает хэш-коды в списки объектов. Функция 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(). Запуск тестового кода с числом, установленным в 10 000, занимает 0.00045 секунд для версии HashQueue и 0,37 секунды для версии Queue. С числом 100 000, версия HashQueue занимает 0.0031 секунды, тогда как Очередь занимает 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();
}

Ответ 7

Почему словарь не подходит?

Чтобы узнать, находится ли конкретное значение в списке, вам нужно пройти весь список. Со словарем (или другим контейнером на основе хэша) гораздо быстрее сузить количество объектов, с которыми вам нужно сравнивать. Ключ (в вашем случае, число) хэшируется и дает словарю дробное подмножество объектов для сравнения.

Ответ 8

Я использую это в Compact Framework, где нет поддержки для HashSet, я выбрал словарь, в котором обе строки - это значение, которое я ищу.

Это означает, что я получаю список < > функциональность со словарной производительностью. Это немного хаки, но это работает.