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

85

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

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 ()?

РЕДАКТИРОВАТЬ: Я должен был быть более ясным по этому поводу: у меня действительно есть список списков, я не знаю, сколько их будет, три приведенных выше списка были просто примером, то, что у меня есть, на самом деле IEnumerable<IEnumerable<SomeClass>>

РЕШЕНИЕ

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

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

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

Оскар
источник

Ответы:

74

Как насчет:

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

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

Джеспер Ларсен-Ледет
источник
1
Вау, я никак не мог подумать об этом решении. Как только у вас будет решение, оно станет очевидным ... хммммм, нет, я оставлю комментарий, чтобы убедиться, что мои коллеги не подумают, что я принимаю слишком много травки :)
Самуэль
функциональная парадигма побеждает)
анатол
зачем нужен скип? Спрашиваю, потому что я не знаю
Исса Фрам
Пропуск существует, потому что первый элемент используется для начального заполнения хеш-набора. Вы должны это сделать, иначе получится куча пересечений с пустым множеством.
SirPentor
Я понимаю решение. Я полагаю, е означает счетчик? Могу я также спросить, что означает h? Полагаю, h означает HashSet?
Quan
63

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

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

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

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

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

РЕДАКТИРОВАТЬ: Для обновленного вопроса:

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();
}
Джон Скит
источник
Да, foreach имеет смысл. Есть ли разница в производительности по сравнению с методом Aggregate в ответе Марселя?
Оскар
@Oskar: Да, в моем ответе используется один хэш-набор вместо того, чтобы каждый раз создавать новый. Однако вы все равно можете использовать Aggregate с набором ... будет редактировать.
Джон Скит,
Крик ... просто попытался разработать решение Aggregate, и это неприятно, потому что HashSet.IntersectWith возвращает null :(
Джон Скит
1
Привет. Один вопрос относительно вашего IntersectAll()метода (который немногочислен): есть ли простой способ добавить селектор в качестве параметра для сравнения значений (например:) Func<TResult, TKey> selectorи по-прежнему использовать InsertectWith()?
tigrou 06
@tigrou: Не так-то просто - потому что вы все равно хотите вернуть a, List<T>а не a List<TKey>, верно? Лучшим подходом, вероятно, было бы создание файла, EqualityComparer<T>который был реализован путем проектирования на TKey.
Джон Скит,
29

Попробуйте, это работает, но я бы очень хотел избавиться от .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());

Обновить:

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

var intersection = listOfLists.Aggregate<IEnumerable<int>>(
   (previousList, nextList) => previousList.Intersect(nextList)
   ).ToList();
Марсель Госселен
источник
Спасибо, я только что попробовал, и это работает! Раньше я не использовал Aggregate (), но я думаю, что это было что-то вроде этого, что я искал.
Оскар
Как я указал в комментарии к ответу Тони, я считаю, что его решение будет работать лучше.
Марсель Госселин
3
Вы можете избавиться от .ToList () в совокупности, если используете Aggregate <IEnumerable <int>>
pomber
@pomber, я не могу поверить, что ваш комментарий прошел 3 года без голосов. Что ж, сегодня твой день, друг мой.
Шон
5

Вы могли бы сделать следующее

var result = list1.Intersect(list2).Intersect(list3).ToList();
ДжаредПар
источник
1
Спасибо, но у меня действительно есть список списков, а не три отдельных списка .. Мне нужно что-то, что работает независимо от того, сколько списков в listOfLists.
Оскар
4
@Oskar Вы можете легко запустить это в цикле
Гейб Мутхарт
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();
гиги
источник
2

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

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

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

Сергей
источник
0

После поиска в сети и не придумав ничего, что мне понравилось (или что работало), я проспал это и придумал это. Мой использует class ( 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();
}

Вот пример, использующий просто список целых чисел, а не класс (это была моя первоначальная реализация).

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();
}
Birdus
источник
-1

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

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

    return input.Aggregate(Enumerable.Intersect);
}
хараким
источник