Перетин кількох списків з IEnumerable.Intersect ()


83

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

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 + агрегат (@Marcel Gosselin), List + foreach (@JaredPar, @Gabe Moothart), HashSet + агрегат (@jesperll) і HashSet + foreach (@Tony the Pony). Я провів тестування продуктивності цих рішень (різна кількість списків , кількість елементів у кожному списку та максимальний розмір випадкової кількості .

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

Для мене агрегований метод справді привабливий (і я погоджуюся з цим як прийняту відповідь), але я б не сказав, що це найбільш читабельне рішення .. Ще раз дякую!

Відповіді:


72

Як на рахунок:

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

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


1
Нічого собі, ніяк, що я міг подумати про це рішення. Як тільки ви отримаєте рішення, це здається очевидним ... хмммм, ні, я залишу коментар, щоб переконатися, що мої колеги не подумають, що я беру занадто багато бур'яну :)
— Самуель

функціональна парадигма перемагає)
— анатол

чому потрібен Skip? Просить, бо не знаю
— Ісса Фрам

Пропуск є, оскільки перший елемент використовується для початкового заповнення хеш-набору. Ви повинні це зробити, бо інакше це купа перетинів з порожнім набором.
— SirPentor

Я розумію рішення. Думаю, e означає «перелічувач»? Чи можу я також запитати, що означає h? Я думаю, h означає HashSet?
— Кван

62

Ви дійсно можете використовувати 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();
}

1
@Skeet "Тоні Поні"?
— Гейб Мутхарт,

Так, foreach має сенс. Будь-яка різниця в продуктивності з цим порівняно з методом агрегатів у відповіді Марселя?
— Оскар

@Oskar: Так, моя відповідь використовує один хешсет замість кожного разу створювати новий. Однак ви все одно можете використовувати агрегат із набором ... буде редагувати.
— Джон Скіт,

Ік ... просто спробував розробити сукупне рішення, і це нестабільно, оскільки HashSet.IntersectWith повертає null :(
— Джон Скіт,

1
Привіт. Одне запитання стосовно вашого IntersectAll()методу (якого є кілька): чи є простий спосіб додати селектор як параметр, порівняти значення (наприклад:) Func<TResult, TKey> selectorі все одно використовувати InsertectWith()?
— tigrou

28

Спробуйте це, це працює, але я б дуже хотів позбутися .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

Це моя версія рішення із методом розширення, який я назвав 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();

4

Ви можете зробити наступне

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

1
Дякую, але у мене справді є список списків, а не три окремі списки .. Мені потрібно щось, що працює незалежно від кількості списків у listOfLists.
— Оскар

4
@Oskar Ви могли б легко запустити це в циклі
— Гейб Мутхарт,

2

Це моє однорядне рішення для Списку Списку (ListOfLists) без функції перетинання:

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

Це має працювати для .net 4 (або пізнішої версії)


0

Після пошуку в мережі і насправді не придумавши чогось, що мені сподобалось (або що спрацювало), я спав на ньому і придумав це. Мій використовує клас ( 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();
}

Ось приклад просто використання списку ints, а не класу (це була моя оригінальна реалізація).

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();
}

-1

Це просто рішення, якщо всі ваші списки невеликі. Якщо у вас більші списки, це не так ефективно, як хеш-набір:

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

    return input.Aggregate(Enumerable.Intersect);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.