簡體   English   中英

比較兩個通用列表差異的最快方法

[英]Quickest way to compare two generic lists for differences

什么是比較兩個大量(> 50.000 項)最快(並且資源占用最少)的方法,結果有兩個列表,如下所示:

  1. 出現在第一個列表中但不在第二個列表中的項目
  2. 出現在第二個列表中但不在第一個列表中的項目

目前我正在使用 List 或 IReadOnlyCollection 並在 linq 查詢中解決此問題:

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

但這並沒有我想要的那么好。 由於我需要處理大量列表,是否有任何想法可以使此過程更快且占用資源更少?

使用Except

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

我懷疑有這實際上是略高於這個速度的方法,但即使這樣會大大超過你的O(N * M)的方法要快。

如果你想結合這些,你可以用上面的方法創建一個方法,然后是一個 return 語句:

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

要注意的一點,有在問題的原代碼和這里的解決方案之間的結果有所不同:其中僅在一個列表中的任何重復的元素將只報告一次我的代碼,而他們會被報告為多它們在原始代碼中出現的次數。

例如,對於[1, 2, 2, 2, 3][1] ,原始代碼中的“list1 中的元素而不是 list2 中的元素”將是[2, 2, 2, 3] 使用我的代碼,它只是[2, 3] 在許多情況下,這不會成為問題,但值得注意。

Enumerable.SequenceEqual 方法

根據相等比較器確定兩個序列是否相等。 文檔

Enumerable.SequenceEqual(list1, list2);

這適用於所有原始數據類型。 如果需要在自定義對象上使用它,則需要實現IEqualityComparer

定義支持比較對象是否相等的方法。

IEqualityComparer 接口

定義支持比較對象是否相等的方法。 IEqualityComparer 的 MS.Docs

更有效的是使用Enumerable.Except

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

該方法是通過使用延遲執行來實現的。 這意味着您可以編寫例如:

var first10 = inListButNotInList2.Take(10);

它也很有效,因為它在內部使用Set<T>來比較對象。 它的工作原理是首先從第二個序列中收集所有不同的值,然后流式傳輸第一個序列的結果,檢查它們之前是否沒有出現過。

如果您希望結果不區分大小寫,則以下操作將起作用:

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

不是針對這個問題,而是這里有一些代碼來比較列表是否相等! 相同的對象:

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);
}
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 的。 為了正確實現IEquatableEqualsGetHashCode必須以邏輯兼容的方式對實例的屬性進行操作。

兩個內容相同的列表仍然是不同的對象,會產生不同的哈希碼。 由於我們希望這兩個列表被視為相等,我們必須讓GetHashCode為它們中的每一個生成相同的值。 我們可以通過將哈希碼委托給列表中的每個元素,並使用標准的按位異或將它們全部組合來實現這一點。 XOR 與順序無關,因此列表的排序方式是否不同都沒有關系。 重要的是它們只包含等效的成員。

注意:奇怪的名字是暗示該方法不考慮列表中元素的順序。 如果您確實關心列表中元素的順序,則此方法不適合您!

試試這個方法:

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

我使用此代碼來比較兩個包含數百萬條記錄的列表。

這個方法不會花太多時間

    //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;
    }

雖然 Jon Skeet 的回答是一個極好的建議,適用於少量到中等數量元素(最多幾百萬)的日常練習,但它並不是最快的方法,而且資源效率也不是很高。 一個明顯的缺點是,要獲得完整的差異需要對數據進行兩次傳遞(如果對相等的元素也感興趣,甚至可以傳遞三次)。 顯然,這可以通過自定義重新實現Except方法來避免,但仍然是創建散列集需要大量內存,並且散列的計算需要時間。

對於非常大的數據集(數十億個元素),考慮特定情況通常是值得的。 以下是一些可能會提供一些啟發的想法: 如果可以比較元素(實踐中幾乎總是如此),那么對列表進行排序並應用以下 zip 方法是值得考慮的:

/// <returns>The elements of the specified (ascendingly) sorted enumerations that are
/// contained only in one of them, together with an indicator,
/// whether the element is contained in the reference enumeration (-1)
/// or in the difference enumeration (+1).</returns>
public static IEnumerable<Tuple<T, int>> FindDifferences<T>(IEnumerable<T> sortedReferenceObjects,
    IEnumerable<T> sortedDifferenceObjects, IComparer<T> comparer)
{
    var refs  = sortedReferenceObjects.GetEnumerator();
    var diffs = sortedDifferenceObjects.GetEnumerator();
    bool hasNext = refs.MoveNext() && diffs.MoveNext();
    while (hasNext)
    {
        int comparison = comparer.Compare(refs.Current, diffs.Current);
        if (comparison == 0)
        {
            // insert code that emits the current element if equal elements should be kept
            hasNext = refs.MoveNext() && diffs.MoveNext();

        }
        else if (comparison < 0)
        {
            yield return Tuple.Create(refs.Current, -1);
            hasNext = refs.MoveNext();
        }
        else
        {
            yield return Tuple.Create(diffs.Current, 1);
            hasNext = diffs.MoveNext();
        }
    }
}

這可以例如以下列方式使用:

const int N = <Large number>;
const int omit1 = 231567;
const int omit2 = 589932;
IEnumerable<int> numberSequence1 = Enumerable.Range(0, N).Select(i => i < omit1 ? i : i + 1);
IEnumerable<int> numberSequence2 = Enumerable.Range(0, N).Select(i => i < omit2 ? i : i + 1);
var numberDiffs = FindDifferences(numberSequence1, numberSequence2, Comparer<int>.Default);

在我的計算機上進行基准測試,N = 1M 的結果如下:

方法 意思 錯誤 標准差 比率 第 0 代 第一代 第 2 代 已分配
DiffLinq 115.19 毫秒 0.656 毫秒 0.582 毫秒 1.00 2800.0000 2800.0000 2800.0000 67110744 乙
壓縮包 23.48 毫秒 0.018 毫秒 0.015 毫秒 0.20 —— —— —— 720 乙

對於 N = 100M:

方法 意思 錯誤 標准差 比率 第 0 代 第一代 第 2 代 已分配
DiffLinq 12.146 秒 0.0427 秒 0.0379 秒 1.00 13000.0000 13000.0000 13000.0000 8589937032 乙
壓縮包 2.324 秒 0.0019 秒 0.0018 秒 0.19 —— —— —— 720 乙

請注意,這個示例當然受益於列表已經排序並且可以非常有效地比較整數的事實。 但這正是重點:如果您確實有有利的環境,請確保您利用它們。

一些進一步的評論:比較函數的速度顯然與整體性能相關,因此對其進行優化可能是有益的。 這樣做的靈活性是壓縮方法的一個好處。 此外,並行化對我來說似乎更可行,盡管絕非易事,而且可能不值得付出努力和開銷。 盡管如此,將過程加速大約 2 倍的簡單方法是將列表分別分成兩半(如果可以有效地完成)並並行比較各部分,一個從前到后處理,另一個處理以相反的順序。

如果只需要組合結果,這也將起作用:

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

其中 T 是列表元素的類型。

我比較了 3 種不同的方法來比較不同的數據集。 下面的測試創建一個字符串集合,其中包含從0length - 1的所有數字,然后是另一個具有相同范圍但偶數數字的集合。 然后我從第一個集合中挑選出奇數。

使用 Linq 除外

public void TestExcept()
{
    WriteLine($"Except {DateTime.Now}");
    int length = 20000000;
    var dateTime = DateTime.Now;
    var array = new string[length];
    for (int i = 0; i < length; i++)
    {
        array[i] = i.ToString();
    }
    Write("Populate set processing time: ");
    WriteLine(DateTime.Now - dateTime);
    var newArray = new string[length/2];
    int j = 0;
    for (int i = 0; i < length; i+=2)
    {
        newArray[j++] = i.ToString();
    }
    dateTime = DateTime.Now;
    Write("Count of items: ");
    WriteLine(array.Except(newArray).Count());
    Write("Count processing time: ");
    WriteLine(DateTime.Now - dateTime);
}

輸出

Except 2021-08-14 11:43:03 AM
Populate set processing time: 00:00:03.7230479
2021-08-14 11:43:09 AM
Count of items: 10000000
Count processing time: 00:00:02.9720879

使用 HashSet.Add

public void TestHashSet()
{
    WriteLine($"HashSet {DateTime.Now}");
    int length = 20000000;
    var dateTime = DateTime.Now;
    var hashSet = new HashSet<string>();
    for (int i = 0; i < length; i++)
    {
        hashSet.Add(i.ToString());
    }
    Write("Populate set processing time: ");
    WriteLine(DateTime.Now - dateTime);
    var newHashSet = new HashSet<string>();
    for (int i = 0; i < length; i+=2)
    {
        newHashSet.Add(i.ToString());
    }
    dateTime = DateTime.Now;
    Write("Count of items: ");
    // HashSet Add returns true if item is added successfully (not previously existing)
    WriteLine(hashSet.Where(s => newHashSet.Add(s)).Count());
    Write("Count processing time: ");
    WriteLine(DateTime.Now - dateTime);
}

輸出

HashSet 2021-08-14 11:42:43 AM
Populate set processing time: 00:00:05.6000625
Count of items: 10000000
Count processing time: 00:00:01.7703057

特殊 HashSet 測試

public void TestLoadingHashSet()
{
    int length = 20000000;
    var array = new string[length];
    for (int i = 0; i < length; i++)
    {
       array[i] = i.ToString();
    }
    var dateTime = DateTime.Now;
    var hashSet = new HashSet<string>(array);
    Write("Time to load hashset: ");
    WriteLine(DateTime.Now - dateTime);
}
> TestLoadingHashSet()
Time to load hashset: 00:00:01.1918160

使用 .Contains

public void TestContains()
{
    WriteLine($"Contains {DateTime.Now}");
    int length = 20000000;
    var dateTime = DateTime.Now;
    var array = new string[length];
    for (int i = 0; i < length; i++)
    {
        array[i] = i.ToString();
    }
    Write("Populate set processing time: ");
    WriteLine(DateTime.Now - dateTime);
    var newArray = new string[length/2];
    int j = 0;
    for (int i = 0; i < length; i+=2)
    {
        newArray[j++] = i.ToString();
    }
    dateTime = DateTime.Now;
    WriteLine(dateTime);
    Write("Count of items: ");
    WriteLine(array.Where(a => !newArray.Contains(a)).Count());
    Write("Count processing time: ");
    WriteLine(DateTime.Now - dateTime);
}

輸出

Contains 2021-08-14 11:19:44 AM
Populate set processing time: 00:00:03.1046998
2021-08-14 11:19:49 AM
Count of items: Hosting process exited with exit code 1.
(Didnt complete. Killed it after 14 minutes)

結論:

  • Linq Except在我的設備上運行比使用HashSets (n=20,000,000) 慢大約 1 秒。
  • 使用WhereContains運行了很長時間

關於 HashSet 的結束語:

  • 唯一數據
  • 確保為類類型覆蓋GetHashCode (正確)
  • 如果您復制數據集,可能需要多達 2 倍的內存,具體取決於實現
  • HashSet 針對使用IEnumerable構造函數克隆其他HashSet進行了優化,但將其他集合轉換為 HashSet 的速度較慢(請參閱上面的特殊測試)

第一種方法:

if (list1 != null && list2 != null && list1.Select(x => list2.SingleOrDefault(y => y.propertyToCompare == x.propertyToCompare && y.anotherPropertyToCompare == x.anotherPropertyToCompare) != null).All(x => true))
   return true;

如果您對重復值沒問題,請使用第二種方法:

if (list1 != null && list2 != null && list1.Select(x => list2.Any(y => y.propertyToCompare == x.propertyToCompare && y.anotherPropertyToCompare == x.anotherPropertyToCompare)).All(x => true))
   return true;

Jon Skeet 和 miguelmpn 的回答都很好。 這取決於列表元素的順序是否重要:

// take order into account
bool areEqual1 = Enumerable.SequenceEqual(list1, list2);

// ignore order
bool areEqual2 = !list1.Except(list2).Any() && !list2.Except(list1).Any();

也許這很有趣,但這對我有用:

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

一條線:

var list1 = new List<int> { 1, 2, 3 };
var list2 = new List<int> { 1, 2, 3, 4 };
if (list1.Except(list2).Count() + list2.Except(list1).Count() == 0)
    Console.WriteLine("same sets");

我做了通用的 function 來比較兩個列表。

 public static class ListTools
{
    public enum RecordUpdateStatus
    {
        Added = 1,
        Updated = 2,
        Deleted = 3
    }


    public class UpdateStatu<T>
    {
        public T CurrentValue { get; set; }
        public RecordUpdateStatus UpdateStatus { get; set; }
    }

    public static List<UpdateStatu<T>> CompareList<T>(List<T> currentList, List<T> inList, string uniqPropertyName)
    {
        var res = new List<UpdateStatu<T>>();

        res.AddRange(inList.Where(a => !currentList.Any(x => x.GetType().GetProperty(uniqPropertyName).GetValue(x)?.ToString().ToLower() == a.GetType().GetProperty(uniqPropertyName).GetValue(a)?.ToString().ToLower()))
            .Select(a => new UpdateStatu<T>
            {
                CurrentValue = a,
                UpdateStatus = RecordUpdateStatus.Added,
            }));

        res.AddRange(currentList.Where(a => !inList.Any(x => x.GetType().GetProperty(uniqPropertyName).GetValue(x)?.ToString().ToLower() == a.GetType().GetProperty(uniqPropertyName).GetValue(a)?.ToString().ToLower()))
            .Select(a => new UpdateStatu<T>
            {
                CurrentValue = a,
                UpdateStatus = RecordUpdateStatus.Deleted,
            }));


        res.AddRange(currentList.Where(a => inList.Any(x => x.GetType().GetProperty(uniqPropertyName).GetValue(x)?.ToString().ToLower() == a.GetType().GetProperty(uniqPropertyName).GetValue(a)?.ToString().ToLower()))
         .Select(a => new UpdateStatu<T>
         {
             CurrentValue = a,
             UpdateStatus = RecordUpdateStatus.Updated,
         }));

        return res;
    }

}

我認為這是一種逐元素比較兩個列表的簡單方法

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)

這是您會找到的最佳解決方案

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM