【问题标题】:Group related list using linq使用 linq 分组相关列表
【发布时间】:2018-08-11 05:41:39
【问题描述】:

我需要知道实现上述结果的最佳方法是什么:

我有以下课程:

public class Log
{
    public HashSet<string> Ids { get; set; }
    public string UniqueId { get; set; }
}
public class GroupModel 
{
    public List<Log> Logs { get; set; }
}

数据集如下:

GroupModel webApiGroupModel = new GroupModel()
        {
            Logs = new List<Log>()
            {
                new Log()
                {
                    Ids = new HashSet<string>(){"a","g"},
                    UniqueId = "1"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){"b", "c" },
                    UniqueId = "2"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){"a", "b"},
                    UniqueId = "3"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){"e"},
                    UniqueId = "4"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){ "d", "e" },
                    UniqueId = "5"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){ "f"},
                    UniqueId = "6"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){ "g"},
                    UniqueId = "7"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){ "a", "g" },
                    UniqueId = "8"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){ "h", "e","g" },
                    UniqueId = "9"

                },
                new Log()
                {
                    Ids = new HashSet<string>(){ },//Intentionally left blank
                    UniqueId = "10"

                },
            }
        };

我需要根据相关的 Id 对它们进行分组,所以结果将如下所示

Group1 = UniqueId 为 1,2,3,4,5,7,8,9 的列表

Group2 = 唯一 ID 为 6 的列表

Group3 = 唯一 ID 为 10 的列表

说明:

Group1:如果任何 Log 与任何 Ids 项匹配,则它们必须组合为一个。由于具有唯一 ID 1 的日志具有 ID“a,g”,这些 ID 存在于具有唯一 ID 3、7、8、9 的日志中,因此这些被组合,但是还有 3、7、8、9 其他项目,即“b,e”,它们是存在于 2 , 4, 5 中,因此所有这些都被分组,即 1,2,3,4,5,7,8,9

Group2,3 Id 不存在于任何日志中,因此它们位于唯一的组中。

我试图以最好的方式实现这一点,因为我使用的解决方案需要 2 分钟才能将数据集与 25K 日志进行分组,这非常糟糕。 每个 Log 中的最大可能 Id 可以是 3。

以下是我的解决方案,您可以帮助我优化它或提供完全不同的方法来实现上述结果。

private static ICollection<List<Log>> GroupIds(List<Log> logs)
    {
        Dictionary<int, HashSet<string>> tempgroupedIds = new Dictionary<int, HashSet<string>>();
        HashSet<string> UniqueIds = new HashSet<string>();
        HashSet<string> tempgroupedIds2 = new HashSet<string>();
        int id = 1;

        foreach (var log in logs)
        {
            List<int> tempGroupNames = new List<int>();
            foreach (var Id in log.Ids)
            {
                if (!string.IsNullOrEmpty(Id))
                {
                    UniqueIds = new HashSet<string>(logs
                        .Where(d => d.Ids.Contains(Id)).Select(p => p.UniqueId));

                    var OtherLogsContainingUniqueIds = tempgroupedIds
                        .Where(d => UniqueIds != null && d.Value.Intersect(UniqueIds).Any())
                        .Select(d => d.Key);
                    if (OtherLogsContainingUniqueIds.Any())
                    {
                        tempGroupNames.AddRange(OtherLogsContainingUniqueIds.ToList());
                    }
                }

                if (tempGroupNames.Any())
                {
                    var tempCorelationids = new HashSet<string>(tempgroupedIds.Where(d => tempGroupNames.Contains(d.Key)).SelectMany(a => a.Value));
                    tempCorelationids.UnionWith(UniqueIds);

                    foreach (var groupname in tempGroupNames)
                    {
                        //If id is found in exiting group put all the Ids from this group to the existing group and remove this group
                        tempgroupedIds.Remove(groupname);
                    }

                    tempgroupedIds2.UnionWith(UniqueIds);
                    tempgroupedIds.Add(id, tempCorelationids);
                    id++;
                }
                else
                {
                    // This a unique group untill some other log is found containing this Id
                    tempgroupedIds2.UnionWith(UniqueIds);
                    tempgroupedIds
                        .Add(id, UniqueIds);
                    id++;
                }
            }
        }

        ICollection<List<Log>> finalGroup = new Collection<List<Log>>();

        foreach (var groupedlogKey in tempgroupedIds)
        {
            var group = logs
                .Where(a => groupedlogKey.Value.Contains(a.UniqueId)).ToList();
            finalGroup.Add(group);
        }

        //With empty Ids
        var anonymousLogs = logs
            .Where(a => !tempgroupedIds2.Contains(a.UniqueId)).ToList();
        if (anonymousLogs.Count >= 1)
        {
            finalGroup.Add(anonymousLogs);
        }

        return finalGroup;
    }
}

【问题讨论】:

  • 如果你能用你尝试过的代码更新问题,我会更好
  • 是的,到目前为止你的解决方案是什么,所以我们知道不会给你同样的解决方案
  • 另外,如果我们不知道你在做什么,我们将如何比较速度
  • 当然,我用我的解决方案更新了问题。
  • 如果您有一个可行的解决方案但想要一些改进(性能、代码风格、架构等),那么最好在Code Review 上提问。

标签: c# algorithm linq grouping


【解决方案1】:

您的算法在输入列表上包含许多内部线性运算,这使其成为二次 O(N*N*K) 时间复杂度(K 的错误足以额外影响性能)。

时间关键部分是通过将每个元素 ID 与每个相交元素 ID 合并来确定唯一的相关 ID 集。

为了有效地做到这一点,我们将使用单通道来构建这样的结构

var idSetById = new Dictionary<string, HashSet<string>>();

以下约束成立:

foreach (var item in idSetById)
{
     Debug.Assert(item.Value.Contains(item.Key));
     foreach (var id in item.Value)
         Debug.Assert(idSetById.ContainsKey(id) && idSetById[id] == idSet);
}

为什么?一是因为它可以在线性时间内创建,二是上述约束允许它用作高效的GroupBy键选择器

.GroupBy(log => idSetById[log.Ids.First()])

(注意:以上适用于非空 id 集。空集将在源迭代过程中简单地添加到单独的列表中)。

这里是完整的方法:

private static ICollection<List<Log>> GroupIds(List<Log> logs)
{
    var emptyIdsGroup = new List<Log>();
    var idSetById = new Dictionary<string, HashSet<string>>();
    var mergeSets = new HashSet<HashSet<string>>();
    foreach (var log in logs)
    {
        if (log.Ids.Count == 0)
        {
            emptyIdsGroup.Add(log);
            continue;
        }
        HashSet<string> idSet = null;
        mergeSets.Clear();
        foreach (var id in log.Ids)
        {
            HashSet<string> mergeSet;
            if (idSetById.TryGetValue(id, out mergeSet))
                mergeSets.Add(mergeSet);
            else
            {
                if (idSet == null) idSet = new HashSet<string>();
                idSet.Add(id);
                idSetById.Add(id, idSet);
            }
        }
        foreach (var mergeSet in mergeSets)
        {
            if (idSet == null)
                idSet = mergeSet;
            else
            {
                // Merge the set with less elements into the set with more elements
                HashSet<string> fromSet;
                if (idSet.Count >= mergeSet.Count)
                    fromSet = mergeSet;
                else
                {
                    fromSet = idSet;
                    idSet = mergeSet;
                }
                foreach (var id in fromSet)
                {
                    idSet.Add(id);
                    idSetById[id] = idSet;
                }
            }
        }
    }

    var groups = logs
        .Where(log => log.Ids.Count > 0)
        .GroupBy(log => idSetById[log.Ids.First()], (key, group) => group.ToList())
        .ToList();

    if (emptyIdsGroup.Count > 0) groups.Add(emptyIdsGroup);

    return groups;
}

由于构建字典和GroupBy操作都具有线性时间复杂度,因此算法的时间复杂度也是线性的。在包含 25,000 个日志的列表上运行它应该需要几毫秒。

编辑:以上内容已经足够好,但还可以进一步优化。合并集合时潜在的代价高昂的操作是更新字典:

foreach (var id in fromSet)
{
    idSet.Add(id);
    idSetById[id] = idSet; // <--
}

可以通过将临时结构内部的HashSet&lt;string&gt; 替换为可以在外部设置的HashSet&lt;string&gt;holding 来避免消耗一些额外的内存:

private class IdSet
{
    public HashSet<string> Ids = new HashSet<string>();
}

所以我们可以改用这样的东西:

idSet.Ids.UnionWith(fromSet.Ids); // merge content
fromSet.Ids = idSet.Ids; // and make both objects have the same content

这也让我们可以一次执行合并操作,并消除了mergeSets 变量的需要。

这是更新后的方法(注意,与第一个实现相比,我们需要使用IdSet.Ids 值作为分组键而不是IdSet 对象):

private static ICollection<List<Log>> GroupIds(List<Log> logs)
{
    var emptyIdsGroup = new List<Log>();
    var idSetById = new Dictionary<string, IdSet>();
    foreach (var log in logs)
    {
        if (log.Ids.Count == 0)
        {
            emptyIdsGroup.Add(log);
            continue;
        }
        IdSet idSet = null;
        foreach (var id in log.Ids)
        {
            IdSet mergeSet;
            if (!idSetById.TryGetValue(id, out mergeSet))
            {
                if (idSet == null) idSet = new IdSet();
                idSet.Ids.Add(id);
                idSetById.Add(id, idSet);
            }
            else if (idSet == null)
                idSet = mergeSet;
            else if (idSet.Ids != mergeSet.Ids)
            {
                // Merge the set with less elements into the set with more elements
                if (idSet.Ids.Count >= mergeSet.Ids.Count)
                {
                    idSet.Ids.UnionWith(mergeSet.Ids);
                    mergeSet.Ids = idSet.Ids;
                }
                else
                {
                    mergeSet.Ids.UnionWith(idSet.Ids);
                    idSet.Ids = mergeSet.Ids;
                    idSet = mergeSet;
                }
            }
        }
    }

    var groups = logs
        .Where(log => log.Ids.Count > 0)
        .GroupBy(log => idSetById[log.Ids.First()].Ids, (key, group) => group.ToList())
        .ToList();

    if (emptyIdsGroup.Count > 0) groups.Add(emptyIdsGroup);

    return groups;
}

【讨论】:

  • 感谢您的解决方案,正如您所说,它以毫秒为单位处理 30k。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-11-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多