Перетин кількох списків з 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.