Найшвидший спосіб порівняння двох загальних списків для відмінностей


214

Який найшвидший (і найменш трудомісткий) для порівняння двох масивних (> 50 000 позицій), і в результаті є два списки, як наведений нижче:

  1. елементи, які відображаються у першому списку, а не у другому
  2. елементи, які відображаються у другому списку, але не в першому

В даний час я працюю зі списком або IReadOnlyCollection і вирішую це питання у запиті на посилання:

var list1 = list.Where(i => !list2.Contains(i)).ToList();
var list2 = list2.Where(i => !list.Contains(i)).ToList();

Але це не так добре, як хотілося б. Будь-яка ідея зробити це більш швидким та менш ресурсоємним, оскільки мені потрібно обробити багато списків?

Відповіді:


454

Використання Except:

var firstNotSecond = list1.Except(list2).ToList();
var secondNotFirst = list2.Except(list1).ToList();

Я підозрюю, що існують підходи, які насправді виявляться незначно швидшими за цей, але навіть це буде набагато швидше, ніж ваш підхід O (N * M).

Якщо ви хочете комбінувати їх, ви можете створити метод із зазначеним вище, а потім повернути:

return !firstNotSecond.Any() && !secondNotFirst.Any();

Слід зазначити, що між початковим кодом у питанні та рішенням тут є різниця в результатах: про будь-які повторювані елементи, що знаходяться лише в одному списку, буде повідомлено лише один раз разом із моїм кодом, тоді як про них буде повідомлено стільки разів, коли вони трапляються в вихідному коді.

Наприклад, зі списками [1, 2, 2, 2, 3]та [1], "початковим кодом" буде результат "елементів у списку1, але не список2" [2, 2, 2, 3]. З моїм кодом це просто було б [2, 3]. У багатьох випадках це не буде проблемою, але варто пам’ятати про це.


8
Це справді величезне підвищення продуктивності! Дякую за цю відповідь.
Франк

2
Мені цікаво два величезні списки, чи корисно сортувати перед порівнянням? або за винятком методу розширення, перерахований список вже відсортований.
Ларрі

9
@Larry: це не відсортовано; він створює хеш-набір.
Джон Скіт

2
@PranavSingh: Він буде працювати для будь-якого, що має відповідну рівність - тому, якщо ваш власний тип переорієнтовує Equals(object)та / або реалізує, IEquatable<T>це має бути добре.
Джон Скіт

2
@ k2ibegin: Він використовує порівняльник рівності за замовчуванням, який використовуватиме IEquatable<T>реалізацію або object.Equals(object)метод. Здається, що вам слід створити нове запитання з мінімально відтворюваним прикладом - ми не можемо реально діагностувати речі в коментарях.
Джон Скіт

40

Більш ефективним було б використання Enumerable.Except:

var inListButNotInList2 = list.Except(list2);
var inList2ButNotInList = list2.Except(list);

Цей метод реалізований за допомогою відкладеного виконання. Це означає, що ви можете написати наприклад:

var first10 = inListButNotInList2.Take(10);

Він також ефективний, оскільки використовує внутрішньо Set<T>для порівняння об'єктів. Вона працює, спочатку збираючи всі різні значення з другої послідовності, а потім передаючи результати першої, перевіряючи, чи не були вони раніше.


1
Хм. Не зовсім відкладено. Я б сказав, частково відкладений. Повна Set<T>складається з другої послідовності (тобто вона повністю ітератована і зберігається), після чого виводяться елементи, які можна додати з першої послідовності.
витрачається

2
@spender, це як би сказати, що виконання. Whereчастково відкладається, оскільки list.Where(x => x.Id == 5)значення числа 5зберігається на початку, а не ліниво виконується.
jwg

27

Спосіб числення

Визначає, чи однакові дві послідовності відповідно до порівняння рівності. MS.Docs

Enumerable.SequenceEqual(list1, list2);

Це працює для всіх примітивних типів даних. Якщо вам потрібно використовувати його на користувацьких об'єктах, вам потрібно реалізуватиIEqualityComparer

Визначає методи підтримки порівняння об'єктів для рівності.

Інтерфейс IEqualityComparer

Визначає методи підтримки порівняння об'єктів для рівності. MS.Docs для IEqualityComparer


це має бути прийнятою відповіддю. Питання стосується не SETS, а списків, які можуть містити дублювання елементів.
Адріан Насуй

3
Я не бачу, як це могло бути відповіддю, враховуючи, що результат SequenceEqualпросто bool. ОП хоче два списки результатів - і описує, чого вони хочуть з точки зору встановлених операцій: "елементи, які відображаються в першому списку, а не у другому". Там немає ніяких ознак того, що порядок має значення, в той час як SequenceEqual дійсно вважають її актуальною. Здається, це відповідає на зовсім інше питання.
Джон Скіт

так, правильно, здається, що я відповів на це занадто швидко і не переглянув другу частину запиту ... так само, як перші два коментарі ...
miguelmpn

9

Якщо ви хочете, щоб результати були нечутливими до регістру , буде діяти наступне:

List<string> list1 = new List<string> { "a.dll", "b1.dll" };
List<string> list2 = new List<string> { "A.dll", "b2.dll" };

var firstNotSecond = list1.Except(list2, StringComparer.OrdinalIgnoreCase).ToList();
var secondNotFirst = list2.Except(list1, StringComparer.OrdinalIgnoreCase).ToList();

firstNotSecondмістив би b1.dll

secondNotFirstмістив би b2.dll


5

Не для цієї проблеми, але ось якийсь код для порівняння списків для рівних, а не! однакові об'єкти:

public class EquatableList<T> : List<T>, IEquatable<EquatableList<T>> where    T : IEquatable<T>

/// <summary>
/// True, if this contains element with equal property-values
/// </summary>
/// <param name="element">element of Type T</param>
/// <returns>True, if this contains element</returns>
public new Boolean Contains(T element)
{
    return this.Any(t => t.Equals(element));
}

/// <summary>
/// True, if list is equal to this
/// </summary>
/// <param name="list">list</param>
/// <returns>True, if instance equals list</returns>
public Boolean Equals(EquatableList<T> list)
{
    if (list == null) return false;
    return this.All(list.Contains) && list.All(this.Contains);
}

1
Це те, що вам потрібно мати можливість порівнювати власні типи даних. Потім використовуйтеExcept
Пранав Сінгх

Напевно, ви можете зробити краще з сортувальними типами. Це працює в O (n ^ 2), тоді як ви можете робити O (nlogn).
yuvalm2

3

спробуйте так:

var difList = list1.Where(a => !list2.Any(a1 => a1.id == a.id))
            .Union(list2.Where(a => !list1.Any(a1 => a1.id == a.id)));

13
Це страждає від жахливої ​​продуктивності, що вимагає сканування другого списку для кожного пункту першого. Не зволікаючи через те, що це працює, але це так само погано, як оригінальний код.
витрачається

3
using System.Collections.Generic;
using System.Linq;

namespace YourProject.Extensions
{
    public static class ListExtensions
    {
        public static bool SetwiseEquivalentTo<T>(this List<T> list, List<T> other)
            where T: IEquatable<T>
        {
            if (list.Except(other).Any())
                return false;
            if (other.Except(list).Any())
                return false;
            return true;
        }
    }
}

Іноді потрібно лише знати, чи два списки різні, а не те, що ці відмінності. У такому випадку розгляньте можливість додавання цього методу розширення до свого проекту. Зауважте, що ваші перелічені об'єкти повинні реалізувати IEquatable!

Використання:

public sealed class Car : IEquatable<Car>
{
    public Price Price { get; }
    public List<Component> Components { get; }

    ...
    public override bool Equals(object obj)
        => obj is Car other && Equals(other);

    public bool Equals(Car other)
        => Price == other.Price
            && Components.SetwiseEquivalentTo(other.Components);

    public override int GetHashCode()
        => Components.Aggregate(
            Price.GetHashCode(),
            (code, next) => code ^ next.GetHashCode()); // Bitwise XOR
}

Незалежно від Componentкласу, показані тут методи Carповинні бути реалізовані майже однаково.

Дуже важливо відзначити, як ми написали GetHashCode. Для того, щоб належним чином реалізувати IEquatable, Equalsі GetHashCode треба працювати на властивості екземпляра в логічно сумісним способом.

Два списки з однаковим вмістом все ще є різними об'єктами, і вони створюватимуть різні хеш-коди. Оскільки ми хочемо, щоб ці два списки розглядалися як рівні, ми повинні дозволити GetHashCodeвиробляти однакове значення для кожного з них. Ми можемо досягти цього, делегувавши хеш-код кожному елементу у списку, і використовувати стандартний побітовий XOR для об'єднання їх усіх. XOR - порядок агностики, тому не має значення, чи списки сортуються по-різному. Важливо лише те, що вони не містять нічого, крім рівнозначних членів.

Зауважте: дивна назва означає те, що метод не враховує порядок елементів у списку. Якщо ви дбаєте про порядок елементів у списку, цей спосіб не для вас!


1

Я використав цей код для порівняння двох списків, що мають мільйон записів.

Цей метод не забере багато часу

    //Method to compare two list of string
    private List<string> Contains(List<string> list1, List<string> list2)
    {
        List<string> result = new List<string>();

        result.AddRange(list1.Except(list2, StringComparer.OrdinalIgnoreCase));
        result.AddRange(list2.Except(list1, StringComparer.OrdinalIgnoreCase));

        return result;
    }

0

Якщо потрібен лише комбінований результат, це також буде працювати:

var set1 = new HashSet<T>(list1);
var set2 = new HashSet<T>(list2);
var areEqual = set1.SetEquals(set2);

де T - тип елемента списків.


-1

Можливо, це смішно, але працює для мене

string.Join ("", List1)! = string.Join ("", List2)


як написано тут, він навіть не буде працювати для List <string> або List <int>, як, наприклад, два списки 11; 2; 3 і 1; 12; 3 були б ідентичними, оскільки ви не з'єднуєте рядки з деякими унікальний роздільник, який не є можливим елементом у списку. Крім того, об'єднуючі рядки для списку з великою кількістю елементів, ймовірно, є вбивцею продуктивності.
SwissCoder

@SwissCoder: Ви помиляєтесь, це не перформанський вбивця для рядка. Якщо у вас є два списки з 50 000 рядків (кожна довжиною 3), цьому алгоритму потрібно 3 мс на моїй машині. Прийняті відповіді потребують 7. Я думаю, що фокус у Джибзі потребує лише одного порівняння рядків. Звичайно, він повинен додати унікальний роздільник.
користувач1027167

@ user1027167: Я не говорю про порівняння рядків безпосередньо (оскільки це також не питання). Виклик методу .ToString () всіх об'єктів у Списку з 50 000 об'єктів може створити величезну рядок, залежно від того, як він реалізований. Я не думаю, що це шлях. Тоді також ризикувати розраховувати на те, що символ або рядок є "унікальним", код насправді не може бути подібним до багаторазового використання.
SwissCoder

Гаразд, це правда. Опитуючий запитував якнайшвидше, не даючи типу даних своїх списків. Можливо, ця відповідь була найшвидшим способом використання запитувача.
користувач1027167

-3

Я думаю, що це простий і простий спосіб порівняння двох списків за елементами

x=[1,2,3,5,4,8,7,11,12,45,96,25]
y=[2,4,5,6,8,7,88,9,6,55,44,23]

tmp = []


for i in range(len(x)) and range(len(y)):
    if x[i]>y[i]:
        tmp.append(1)
    else:
        tmp.append(0)
print(tmp)

3
Це питання C #, і ви не вказали код C #.
Вай Ха Лі

1
Можливо, ви зможете видалити цю відповідь і перемістити її до (наприклад) Як я можу порівняти два списки в python і повернути збіги ?
Вай Ха Лі

-4

Це найкраще рішення, яке ви знайдете

var list3 = list1.Where(l => list2.ToList().Contains(l));

1
Це насправді дуже погано, оскільки це створює новий List<T>для кожного елемента в list1. Також результат називається, list3коли він не є List<T>.
Вай Ха Лі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.