сопоставление элементов из двух списков (или массивов) - PullRequest
19 голосов
/ 07 января 2009

У меня проблема с работой, которая, я надеюсь, сводится к следующему: у меня есть два List<int> с, и я хочу посмотреть, равно ли любое из int в ListA любому int в ListB. (Они могут быть массивами, если это облегчает жизнь, но я думаю, что List<> имеет некоторую встроенную магию, которая может помочь.) И я уверен, что это дружественная к LINQ проблема, но я работаю в 2.0 здесь.

Мое решение до сих пор было foreach через ListA, а затем foreach через ListB,

foreach (int a in ListA)
{
    foreach (int b in ListB)
    {
        if (a == b)
        {
            return true;
        }
    }
}

, который на самом деле был довольно гладким, когда каждый из них имел длину три элемента, но теперь их длина 200, и они часто не совпадают, поэтому мы получаем наихудший случай сравнения N ^ 2. Даже 40 000 сравнений проходят довольно быстро, но я думаю, что я что-то упускаю, поскольку N ^ 2 кажется довольно наивным для этой конкретной проблемы.

Спасибо!

Ответы [ 5 ]

38 голосов
/ 07 января 2009

С LINQ это тривиально, так как вы можете вызвать Intersect метод расширения в Enumerable классе , чтобы дать вам установленное пересечение из двух массивов:

var intersection = ListA.Intersect(ListB);

Однако это пересечение set , означающее, что если ListA и ListB не содержат уникальных значений, вы не получите никаких копий. Другими словами, если у вас есть следующее:

var ListA = new [] { 0, 0, 1, 2, 3 };
var ListB = new [] { 0, 0, 0, 2 };

Тогда ListA.Intersect(ListB) производит:

{ 0, 2 }

Если вы ожидаете:

{ 0, 0, 2 }

Тогда вам придется самостоятельно вести подсчет предметов и давать / уменьшать при сканировании двух списков.

Сначала вам нужно собрать Dictionary<TKey, int> со списками отдельных предметов:

var countsOfA = ListA.GroupBy(i => i).ToDictionary(g => g.Key, g => g.Count());

Оттуда вы можете отсканировать ListB и поместить его в список, когда натолкнетесь на элемент в countsOfA:

// The items that match.
IList<int> matched = new List<int>();

// Scan 
foreach (int b in ListB)
{
    // The count.
    int count;

    // If the item is found in a.
    if (countsOfA.TryGetValue(b, out count))
    {
        // This is positive.
        Debug.Assert(count > 0);

        // Add the item to the list.
        matched.Add(b);

        // Decrement the count.  If
        // 0, remove.
        if (--count == 0) countsOfA.Remove(b);
    }
}

Вы можете заключить это в метод расширения, который откладывает выполнение следующим образом:

public static IEnumerable<T> MultisetIntersect(this IEnumerable<T> first,
    IEnumerable<T> second)
{
    // Call the overload with the default comparer.
    return first.MultisetIntersect(second, EqualityComparer<T>.Default);
}

public static IEnumerable<T> MultisetIntersect(this IEnumerable<T> first,
    IEnumerable<T> second, IEqualityComparer<T> comparer)
{
    // Validate parameters.  Do this separately so check
    // is performed immediately, and not when execution
    // takes place.
    if (first == null) throw new ArgumentNullException("first");
    if (second == null) throw new ArgumentNullException("second");
    if (comparer == null) throw new ArgumentNullException("comparer");

    // Defer execution on the internal
    // instance.
    return first.MultisetIntersectImplementation(second, comparer);
}

private static IEnumerable<T> MultisetIntersectImplementation(
    this IEnumerable<T> first, IEnumerable<T> second, 
    IEqualityComparer<T> comparer)
{
    // Validate parameters.
    Debug.Assert(first != null);
    Debug.Assert(second != null);
    Debug.Assert(comparer != null);

    // Get the dictionary of the first.
    IDictionary<T, long> counts = first.GroupBy(t => t, comparer).
        ToDictionary(g => g.Key, g.LongCount(), comparer);

    // Scan 
    foreach (T t in second)
    {
        // The count.
        long count;

        // If the item is found in a.
        if (counts.TryGetValue(t, out count))
        {
            // This is positive.
            Debug.Assert(count > 0);

            // Yield the item.
            yield return t;

            // Decrement the count.  If
            // 0, remove.
            if (--count == 0) counts.Remove(t);
        }
    }
}

Обратите внимание, что оба эти подхода являются (и я прошу прощения, если я здесь использую нотацию Big-O) O(N + M), где N - количество элементов в первом массиве, а M - количество элементы во втором массиве. Вы должны сканировать каждый список только один раз, и предполагается, что получение хеш-кодов и поиск хеш-кодов - это O(1) (постоянная) операция.

7 голосов
/ 07 января 2009

Загрузите весь ListA в экземпляр HashSet, а затем протестируйте элемент foreach в ListB против HastSet: я почти уверен, что это будет O (N).

//untested code ahead
HashSet<int> hashSet = new HashSet<int>(ListA);
foreach (int i in ListB)
{
    if (hashSet.Contains(i))
        return true;
}

Вот то же самое в одной строке:

return new HashSet<int>(ListA).Overlaps(ListB);

HashSet не существует в .NET 3.5, поэтому в .NET 2.0 вы можете использовать Dictionary<int,object> (вместо HashSet<int>) и всегда сохранять значение null в качестве объекта / значения в Словаре, поскольку вы только интересует ключи.

3 голосов
/ 07 января 2009

Вместо того, чтобы перебирать каждый список, взгляните на метод List.Contains :

foreach (int a in ListA)
{
  if (ListB.Contains(a))
    return true;
}
2 голосов
/ 07 января 2009

Крис дает решение O (N) путем хеширования. Теперь, в зависимости от постоянного фактора (из-за хеширования), возможно, стоит рассмотреть решение O (N log (N)) путем сортировки. Есть несколько различных вариантов, которые вы можете рассмотреть в зависимости от вашего варианта использования.

  1. Сортировка ListB (O (N log (N)) и использование алгоритма поиска для разбора каждого элемента в ListA (что опять-таки O (N) * O (log (N))).

  2. Сортировка ListA и ListB (O (N log (N))) и использование алгоритма O (N) для сравнения этих списков на наличие дубликатов.

Если оба списка будут использоваться более одного раза, предпочтительным является второй метод.

0 голосов
/ 07 января 2009

Как насчет использования метода BinarySearch вместо перебора всех элементов во внутреннем цикле?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...