条目不唯一的两个列表之间的差异

时间:2018-09-24 07:38:43

标签: c# linq

我有两个刺痛。一种代表我的代码可以运行的功能类型,另一种代表可以运行这些功能的代理。这两个列表应该是一对一的关系,但是当远程服务需要更多功能时,我需要找出这两个列表之间的区别。 问题在于条目不是唯一的,所以我不能只调用list1.RemoveAll(list2),因为这将删除List2中包含相同值的所有条目,而不是每个条目仅删除一个。

这就是我需要的:

{a,a,a,a,b,b,c} - {a,a,b,c} = {a,a,b}

这就是我现在的做法:

var difference = list1.ToList();
foreach (var entry in list2)
{
    difference.Remove(entry);
}

它可以正常工作,但在其余的代码中却破坏了Linq的用法。

我试图找到一种方法并在线搜索,但是找不到使用Linq进行搜索的方法。

2 个答案:

答案 0 :(得分:4)

如果 long 个集合(序列)的嵌套循环和Remove无效(从O (N * M)O(N * N * M)),则可以尝试{{1 }}和grouping,时间复杂度为dictionary。请注意,实现不保留初始顺序(O (N + M),而不是{a, b, b, a} - {b} == {a, a, b}

{a, b, a}

编辑:创建基准很容易;如果是长序列(List<char> left = new List<char>() { 'a', 'a', 'a', 'a', 'b', 'b', 'c' }; List<char> right = new List<char>() { 'a', 'a', 'b', 'c' }; var counts = right .GroupBy(item => item) .ToDictionary(chunk => chunk.Key, chunk => chunk.Count()); var difference = left .GroupBy(item => item) .SelectMany(chunk => chunk.Skip(counts.TryGetValue(chunk.Key, out var skip) ? skip : 0)) .ToList(); ),则以 hash N = 200000GroupBy)为准;确保如果您没有那么长的收藏夹,则可以放心地保留初始代码:

Dictionary

现在让马跑:

Random rnd = new Random(1);

int N = 200000;

List<char> left = Enumerable
  .Range(0, N)
  .Select(index => (char)(rnd.Next('z' - 'a') + 'a'))
  .ToList();

List<char> right = Enumerable
  .Range(0, N)
  .Select(index => (char)(rnd.Next('z' - 'a') + 'a'))
  .ToList();

结果(Core i7 3.6GHz)Stopwatch watch = new Stopwatch(); watch.Start(); // Hash solution var counts = right .GroupBy(item => item) .ToDictionary(chunk => chunk.Key, chunk => chunk.Count()); var result = left .GroupBy(item => item) .SelectMany(chunk => chunk.Skip(counts.TryGetValue(chunk.Key, out var skip) ? skip : 0)) .ToList(); watch.Stop(); TimeSpan tHash = watch.Elapsed; watch.Reset(); watch.Start(); // Initial solution var difference = left.ToList(); foreach (var entry in right) { difference.Remove(entry); } watch.Stop(); TimeSpan tInitial = watch.Elapsed; Console.Write($"Hash: {tHash}; Initial {tInitial}"); 11 ms

1.4 second

答案 1 :(得分:1)

我不确定您的代码是否满足您的要求:

var list1 = {b, b, c, a};
var list2 = {a, b, b, c};

即使第二个列表中的顺序与第一个列表中的顺序不同,您的代码也会删除所有元素。

var list1 = {a, b, a, c, a};
var list2 = {a, a, b, c};
var list3 = {b, c, a, a}

list1-list2和list1-list3将具有相同的输出:

result = {b, c, a}

这是您想要的吗?订单重要吗?

除了代码之外,还可以更改输入数据。 LINQ用于查询数据,没有LINQ函数曾经更改过输入数据。如果您确实希望代码更改输入数据,则无法将其转换为类似LINQ的函数。

但是,如果您不想更改输入序列,我们可以使用一个新功能来“扩展” IEnumerable的功能,该功能可以像LINQ函数一样执行您的功能,但它不会更改输入顺序。

该函数将有两个IEnumerable<TSource>作为输入,并返回一个IEnumerable<TSource>作为输出。输入顺序不变。

请参见Extension Methods Demystified

static class EnumerableExtensions
{
    public static IEnumerable<TSource> RemoveDuplicates<TSource> (
        this IEnumerable<Tsource> list1,
        IEnumerable<TSource> list2)
    {
         // TODO: implement
    }
}

(我想不出一个合适的名字)

用法:

IEnumerable<string> list1 = ...
IEnumerable<string> list2 = ...

IEnumerable<string> result = list1.RemoveDuplicates(list2);

或者在复杂的LINQ函数中:

var result = list1.Where(x => x.StartsWith("a")
   .RemoveDuplicates(list2.Where(x => x.EndsWith("z")
   .Select(x => ...)
   .ToList();

如果这是您想要的,让我们实现它。

public static IEnumerable<TSource> RemoveDuplicates<TSource> (
    this IEnumerable<Tsource> list1,
    IEnumerable<TSource> list2)
{
    var differenct = list1.ToList();
    foreach (var entry in list2)
    {
        difference.Remove(entry);
    }
    return difference;
}

这不是很有效。例如,如果您只想用结果Any()FirstOrDefault(),则计算完整列表效率不高。

如果结果的顺序并不重要,则可以按相同的字符串对输入进行分组,并仅返回元素数量之差。因此,如果list1有5个“ a”,而list2有3个“ a”,则返回2次“ a”。为此,您可以对字符串进行分组,并计算每个组中的项目数。

为简单起见,我的评论就像我们从您的输入中删除了几个“ a”值

public static IEnumerable<TSource> RemoveDuplicates<TSource> (
    this IEnumerable<Tsource> list1,
    IEnumerable<TSource> list2)
{
    var group1 = list1.GroupBy(item => item)
       .Select(group => new
       {
           value = group.Key,
           count = group.Count(),
       });

    var group2 = list2.GroupBy(item => item)
       .Select(group => new
       .ToDictionary(group => group.Key, group => group.Count());

    // for every item in group1, check if there is a same one in group2.
    // If so, subtract the count and return the remaining items
    foreach (var item in group1)
    {
        // are the also some "a" values in list2?
        if (group2.TryGetValue(item1.Value, out int nrToremove))
        {
            // yes there are: nrToRemove contains the number of "a" values in list2
            int nrToReturn = item.Count - nrToRemove;

            // return all remaining "a" values:
            for (int i=0; i<nrToReturn; ++i)
            {
                yield return item.Value;  // return an "a"
            }
        }
    }
}

请注意,由于yield语句,此函数使用延迟执行。仅当您开始枚举LINQ时,它将被执行。

因为要删除的“ a”可能是list2中的最后一个元素,所以要获取第一个返回的元素,我们必须枚举list2的所有元素以检查第一个“ a”是否在list2中的任何位置。因为我记得该枚举的结果,所以不必再次枚举list2即可返回第二个(和其他任何元素)。