我有以下情况:
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 }
当然,你可以输出任何你想要的东西。正如您所看到的,唯一需要说明具有两个匹配元素的地方是查询的第一部分。