Пересечение нескольких списков с помощью IEnumerable.Intersect()

У меня есть список списков, которые я хочу найти для такого пересечения:

var list1 = new List<int>() { 1, 2, 3 };
var list2 = new List<int>() { 2, 3, 4 };
var list3 = new List<int>() { 3, 4, 5 };
var listOfLists = new List<List<int>>() { list1, list2, list3 };

// expected intersection is List<int>() { 3 };

Есть ли способ сделать это с помощью IEnumerable.Intersect()?

EDIT: Я должен был быть более ясным в этом: у меня действительно есть список списков, я не знаю, сколько их будет, три списка выше были просто примером, что у меня есть на самом деле IEnumerable<IEnumerable<SomeClass>>

Решение

Спасибо за отличные ответы. Оказалось, что существует четыре варианта решения этого вопроса: Список + агрегат (@Marcel Gosselin), Список + foreach (@JaredPar, @Gabe Moothart), HashSet + агрегат (@jesperll) и HashSet + foreach (@Tony the Pony). Я провел некоторое тестирование производительности этих решений (варьируя количество списков, количество элементов в каждом списке и случайное число max.

Оказывается, что для большинства ситуаций HashSet работает лучше, чем List (за исключением больших списков и небольшого размера случайных чисел, из-за природы HashSet, я думаю). Я не мог найти никакой реальной разницы между методом foreach и агрегированным методом (метод foreach работает немного лучше.)

Для меня агрегированный метод действительно привлекателен (и я согласен с этим как принятый ответ), но я бы не сказал, что это наиболее читаемое решение.. Еще раз спасибо!

Ответ 1

Как насчет:

var intersection = listOfLists
    .Skip(1)
    .Aggregate(
        new HashSet<T>(listOfLists.First()),
        (h, e) => { h.IntersectWith(e); return h; }
    );

Таким образом, он оптимизируется с использованием одного и того же HashSet во всем и все еще в одном выражении. Просто убедитесь, что listOfLists всегда содержит хотя бы один список.

Ответ 2

Вы действительно можете использовать Intersect дважды. Однако я считаю, что это будет более эффективно:

HashSet<int> hashSet = new HashSet<int>(list1);
hashSet.IntersectWith(list2);
hashSet.IntersectWith(list3);
List<int> intersection = hashSet.ToList();

Не проблема с небольшими наборами, конечно, но если у вас много больших наборов, это может быть значительным.

В основном Enumerable.Intersect необходимо создать набор для каждого вызова - если вы знаете, что собираетесь делать больше операций с множеством, вы можете также сохранить это значение.

Как всегда, внимательно следите за производительностью и читабельностью - метод цепочки вызова Intersect дважды очень привлекателен.

EDIT: для обновленного вопроса:

public List<T> IntersectAll<T>(IEnumerable<IEnumerable<T>> lists)
{
    HashSet<T> hashSet = null;
    foreach (var list in lists)
    {
        if (hashSet == null)
        {
            hashSet = new HashSet<T>(list);
        }
        else
        {
            hashSet.IntersectWith(list);
        }
    }
    return hashSet == null ? new List<T>() : hashSet.ToList();
}

Или, если вы знаете, что он не будет пустым, и что Skip будет относительно дешевым:

public List<T> IntersectAll<T>(IEnumerable<IEnumerable<T>> lists)
{
    HashSet<T> hashSet = new HashSet<T>(lists.First());
    foreach (var list in lists.Skip(1))
    {
        hashSet.IntersectWith(list);
    }
    return hashSet.ToList();
}

Ответ 3

Попробуй, это работает, но я бы очень хотел избавиться от .ToList() в совокупности.

var list1 = new List<int>() { 1, 2, 3 };
var list2 = new List<int>() { 2, 3, 4 };
var list3 = new List<int>() { 3, 4, 5 };
var listOfLists = new List<List<int>>() { list1, list2, list3 };
var intersection = listOfLists.Aggregate((previousList, nextList) => previousList.Intersect(nextList).ToList());

Update:

Следуя за комментарием @pomber, можно избавиться от ToList() внутри вызова Aggregate и переместить его за пределы, чтобы выполнить его только один раз. Я не тестировал, был ли предыдущий код быстрее нового. Необходимое изменение состоит в том, чтобы указать параметр типового типа метода Aggregate в последней строке, как показано ниже:

var intersection = listOfLists.Aggregate<IEnumerable<int>>(
   (previousList, nextList) => previousList.Intersect(nextList)
   ).ToList();

Ответ 4

Вы можете сделать следующее

var result = list1.Intersect(list2).Intersect(list3).ToList();

Ответ 5

Это моя версия решения с методом расширения, который я назвал IntersectMany.

public static IEnumerable<TResult> IntersectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
    using (var enumerator = source.GetEnumerator())
    {
        if(!enumerator.MoveNext())
            return new TResult[0];

        var ret = selector(enumerator.Current);

        while (enumerator.MoveNext())
        {
            ret = ret.Intersect(selector(enumerator.Current));
        }

        return ret;
    }
}

Таким образом, использование будет примерно таким:

var intersection = (new[] { list1, list2, list3 }).IntersectMany(l => l).ToList();

Ответ 6

Это мое однострочное решение для списка List (ListOfLists) без функции пересечения:

var intersect = ListOfLists.SelectMany(x=>x).Distinct().Where(w=> ListOfLists.TrueForAll(t=>t.Contains(w))).ToList()

Это должно работать для .net 4 (или более поздней версии)

Ответ 7

После поиска "сети" и совсем не придумал что-то, что мне понравилось (или это сработало), я спал на нем и придумал это. Mine использует класс (SearchResult), который имеет EmployeeId в нем и что вещь, которая должна быть общей в списках. Я возвращаю все записи с EmployeeId в каждом списке. Это не фантазия, но это просто и легко понять, только то, что мне нравится. Для небольших списков (мой случай) он должен выполняться просто отлично, и каждый может это понять!

private List<SearchResult> GetFinalSearchResults(IEnumerable<IEnumerable<SearchResult>> lists)
{
    Dictionary<int, SearchResult> oldList = new Dictionary<int, SearchResult>();
    Dictionary<int, SearchResult> newList = new Dictionary<int, SearchResult>();

    oldList = lists.First().ToDictionary(x => x.EmployeeId, x => x);

    foreach (List<SearchResult> list in lists.Skip(1))
    {
        foreach (SearchResult emp in list)
        {
            if (oldList.Keys.Contains(emp.EmployeeId))
            {
                newList.Add(emp.EmployeeId, emp);
            }
        }

        oldList = new Dictionary<int, SearchResult>(newList);
        newList.Clear();
    }

    return oldList.Values.ToList();
}

Вот пример, просто используя список int, а не класс (это была моя оригинальная реализация).

static List<int> FindCommon(List<List<int>> items)
{
    Dictionary<int, int> oldList = new Dictionary<int, int>();
    Dictionary<int, int> newList = new Dictionary<int, int>();

    oldList = items[0].ToDictionary(x => x, x => x);

    foreach (List<int> list in items.Skip(1))
    {
        foreach (int i in list)
        {
            if (oldList.Keys.Contains(i))
            {
                newList.Add(i, i);
            }
        }

        oldList = new Dictionary<int, int>(newList);
        newList.Clear();
    }

    return oldList.Values.ToList();
}

Ответ 8

Это простое решение, если ваши списки невелики. Если у вас более крупные списки, это не так, как выполнение хеш-набора:

public static IEnumerable<T> IntersectMany<T>(this IEnumerable<IEnumerable<T>> input)
{
    if (!input.Any())
        return new List<T>();

    return input.Aggregate(Enumerable.Intersect);
}