比较两个列表的差异

本文关键字:列表 两个 比较 | 更新日期: 2023-09-27 17:58:19

我有以下情况:

class A
{
   public A(string name, int age) { Name = name; Age = age; }
   public string Name;
   public int Age;
}
List<A> one = 
  new List<A>() { new A("bob", 15), new A("john", 10), new A("mary", 12) };
List<A> two = 
  new List<A>() { new A("bob", 15), new A("mary", 15), new A("cindy", 18) }; 

我想在这些列表之间做一个比较,并得到信息,约翰只在列表1中,辛迪只在列表2中,玛丽在两个列表中,但这并不完全匹配(年龄不同)。我的目标是将这些信息进行并排比较。

有人能建议如何有效地做到这一点吗?

如果我错过了任何重复的问题,我很抱歉,我能找到的问题只涉及布尔结果,而不是实际的细节。

比较两个列表的差异

var result = 
    one.Select(a => Tuple.Create(a, "one")) // tag list one items
    .Concat(two.Select(a => Tuple.Create(a, "two"))) // tag list two items
    .GroupBy(t => t.Item1.Name) // group items by Name
    .ToList(); // cache result
var list_one_only = result.Where(g => g.Count() == 1 && g.First().Item2 == "one");
var list_two_only = result.Where(g => g.Count() == 1 && g.First().Item2 == "two");
var both_list_diff = result.Where(g => g.Count() == 2 && g.First().Item1.Age != g.Skip(1).First().Item1.Age);

这将返回一个列表列表,其中每个内部列表将是1个项目(具有原始项目的元组以及它来自哪个列表),或者将有2个项目(相同的名称,可能相同的年龄,以及哪个年龄来自哪个列表。

我不确定你到底想要什么结构的结果,所以我把它留在了那里。否则,您可以从这里进行另一次选择,以过滤掉两个列表中的相同记录("bob")等。

这个解决方案应该只遍历两个列表一次。

传递可以是隐式的或"显式的"。我所说的显式是指通过一些Linq扩展方法隐藏。因此,您可以执行以下操作:

var results = (from item in one.Concat(two).Select(x => x.Name).Distinct()
                let inFirst = one.Find(x => x.Name == item)
                let inSecond = two.Find(x => x.Name == item)
                let location = inFirst != null 
                            && inSecond != null 
                                ? 2 : inSecond != null ? 1 : 0
                select new
                {
                    Name = item,
                    location = location == 0 ? "First" : location == 1 ? "Second" : "Both",
                    ExactMatch = (location != 2 || inFirst.Age == inSecond.Age) 
                                  ? "YES" : $"One: { inFirst.Age } | Two: { inSecond.Age }"
                }).ToList();

结果:

{ Name = bob, location = Both, ExactMatch = YES }
{ Name = john, location = First, ExactMatch = YES }
{ Name = mary, location = Both, ExactMatch = One: 12 | Two: 15 }
{ Name = cindy, location = Second, ExactMatch = YES }

如果您担心性能,请使用有效的数据结构查找O(1)。以下内容在10000项目列表的33ms中完成,而以上内容在5000ms:附近完成

var oneLookup = one.ToLookup(x => x.Name, x => x);
var twoLookup = two.ToLookup(x => x.Name, x => x);
var results = (from item in one.Concat(two).Select(x => x.Name).Distinct()
                let inFirst = oneLookup[item].FirstOrDefault()
                let inSecond = twoLookup[item].FirstOrDefault()
                let location = inFirst != null
                            && inSecond != null
                                ? 2 : inSecond != null ? 1 : 0
                select new
                {
                    Name = item,
                    location = location == 0 ? "First" : location == 1 ? "Second" : "Both",
                    ExactMatch = (location != 2 || inFirst.Age == inSecond.Age)
                                  ? "YES" : $"One: { inFirst.Age } | Two: { inSecond.Age }"
                }).ToList();

有人能建议如何有效地做到这一点吗?

在一次传递中产生差异的唯一方法是,如果两个序列按身份密钥(在您的情况下为Name)排序。然而,订购会带来额外的成本,而且该过程无法在LINQ中编码。

您真正需要的是full outer join,它没有天生的LINQ支持。因此,经典方法需要两次通过——left other join用于一次中存在并最终在第二次中存在的东西,right antijoin用于仅在第二秒内存在的东西。这是迄今为止最有效的LINQ方式。

查询可能是这样的:

var result =
    (from e1 in one
     join e2 in two on e1.Name equals e2.Name into match
     from e2 in match.DefaultIfEmpty()
     select new
     {
         e1.Name,
         In = e2 == null ? "A" : "A,B",
         Age = e2 == null || e1.Age == e2.Age ? e1.Age.ToString() : $"A:{e1.Age} B:{e2.Age}"
     })
    .Concat
    (from e2 in two
     join e1 in one on e2.Name equals e1.Name into match
     where !match.Any()
     select new { e2.Name, In = "B", Age = e2.Age.ToString() })
    .ToList();

它从您的样本数据中生成以下内容:

{ Name = bob, In = A,B, Age = 15 }
{ Name = john, In = A, Age = 10 }
{ Name = mary, In = A,B, Age = A:12 B:15 }
{ Name = cindy, In = B, Age = 18 }

当然,你可以输出任何你想要的东西。正如您所看到的,唯一需要说明具有两个匹配元素的地方是查询的第一部分。