【问题标题】:Merge elements in IEnumerable according to a condition根据条件合并 IEnumerable 中的元素
【发布时间】:2011-03-30 02:25:07
【问题描述】:

我一直在寻找一些快速有效的方法来合并数组中的项目。这是我的场景。集合按发件人排序。相邻元素不一定相差 1,即最后一个 To 和下一个 From 之间可能存在间隙,但它们永远不会重叠。

var list = new List<Range>();
list.Add(new Range() { From = 0, To = 1, Category = "AB" });
list.Add(new Range() { From = 2, To = 3, Category = "AB" });
list.Add(new Range() { From = 4, To = 5, Category = "AB" });
list.Add(new Range() { From = 6, To = 8, Category = "CD" });
list.Add(new Range() { From = 9, To = 11, Category = "AB" }); // 12 is missing, this is ok
list.Add(new Range() { From = 13, To = 15, Category = "AB" });

我希望以这样的方式合并上述集合,即前三个(这个数字可以变化,从至少 2 个元素到满足条件的多个元素)元素成为一个元素。无法合并具有不同类别的元素。

new Range() { From = 0, To = 5, Category = "AB" };

这样生成的集合总共有 4 个元素。

0 - 5    AB
6 - 8    CD
9 - 11   AB // no merging here, 12 is missing
13 - 15  AB

我有一个非常大的集合,包含超过 2.000.000 项,我希望尽可能高效地完成此操作。

【问题讨论】:

  • 列表是否已经以相邻范围彼此相邻且在列表中的正确顺序进行排序?
  • 是的,我为此添加了 cmets。
  • 我假设在您的示例中,前两个范围只能合并,因为它们具有相同的类别,并且 To 和 From 最多相差 1?如果第一个范围以 2 结尾,它们不能合并?
  • 正确。缺少通知编号 12,并且元素未合并。此外,可以合并更多连续的元素,而不仅仅是 2 个。如果存在间隙(具有相同类别),则合并将停止。我在原始代码中添加了更多示例来阐明这一点。
  • 我很困惑。你要求一个“尽可能高效”的解决方案,但你接受了通用的、基于委托的解决方案,这会更慢。如果您能在对我的回答的评论中解释为什么您认为它有缺陷,我会很高兴,这样我就可以为未来学习。谢谢!

标签: c# .net algorithm collections


【解决方案1】:

这是另一个:

IEnumerable<Range> Merge(IEnumerable<Range> input)
{
    input = input.OrderBy(r => r.Category).ThenBy(r => r.From).ThenBy(r => r.To).ToArray();
    var ignored = new HashSet<Range>();
    foreach (Range r1 in input)
    {
        if (ignored.Contains(r1))
            continue;

        Range tmp = r1;
        foreach (Range r2 in input)
        {
            if (tmp == r2 || ignored.Contains(r2))
                continue;

            Range merged;
            if (TryMerge(tmp, r2, out merged))
            {
                tmp = merged;
                ignored.Add(r1);
                ignored.Add(r2);
            }
        }
        yield return tmp;
    }
}

bool TryMerge(Range r1, Range r2, out Range merged)
{
    merged = null;
    if (r1.Category != r2.Category)
        return false;
    if (r1.To + 1 < r2.From || r2.To + 1 < r1.From)
        return false;
    merged = new Range
    {
        From = Math.Min(r1.From, r2.From),
        To = Math.Max(r1.To, r2.To),
        Category = r1.Category
    };
    return true;
}

你可以直接使用它:

var mergedList = Merge(list);

但是如果你有很多项目,因为复杂度是 O(n²),那将是非常低效的。但是,由于只能合并同一类别的项目,因此可以将它们按类别分组并合并每个组,然后将结果展平:

var mergedList = list.GroupBy(r => r.Category)
                    .Select(g => Merge(g))
                    .SelectMany(g => g);

【讨论】:

    【解决方案2】:

    假设列表已排序 - 并且 - 范围不重叠,正如您在问题中所述,这将在 O(n) 时间内运行:

    var flattenedRanges = new List<Range>{new Range(list.First())};
    
    foreach (var range in list.Skip(1))
    {
        if (flattenedRanges.Last().To + 1 == range.From && flattenedRanges.Last().Category == range.Category)
            flattenedRanges.Last().To = range.To;
        else
            flattenedRanges.Add(new Range(range));
    }
    

    这是假设您有一个 Range 的复制构造函数

    编辑: 这是一个就地算法:

        for (int i = 1; i < list.Count(); i++)
        {
            if (list[i].From == list[i - 1].To+1  && list[i-1].Category == list[i].Category)
            {
                list[i - 1].To = list[i].To;
                list.RemoveAt(i--);
            }
        }
    

    编辑:

    添加了分类检查,并修复了就地版本。

    【讨论】:

    • ① 此解决方案将输入集合丢弃。我假设输入集合应该保持不变。 ② 此方案忘记比较类别。
    • @Timwi:使用带索引的 for 循环是一个小改动。
    • @Ani:嗯?您正在修改输入集合中的对象,使用 for 循环进行微小更改如何解决该问题?
    • @Ani:无论您使用for 循环还是foreach 都没有区别。无论哪种方式,您都在破坏输入集合。
    • 哦,此外,如果 Range 是值类型,您的解决方案将无法编译。
    【解决方案3】:

    这是一个通用的、可重复使用的解决方案,而不是一个临时的、特定的解决方案。 (根据cmets更新)

    IEnumerable<T> Merge<T>(this IEnumerable<T> coll, 
                          Func<T,T,bool> canBeMerged, Func<T,T,T>mergeItems)
    {
        using(IEnumerator<T> iter = col.GetEnumerator())
        {
          if (iter.MoveNext())
          {
              T lhs = iter.Current;
              while(iter.MoveNext())
              {
                  T rhs = iter.Current;
                  if (canBeMerged(lhs, rhs)
                     lhs=mergeItems(lhs, rhs);
                  else
                  {
                     yield return lhs;
                     lhs= rhs;
                  }
              }
              yield return lhs;
          }
        }
    }
    

    您必须提供方法来确定项目是否可以合并,并合并它们。 这些确实应该是 Range 类的一部分,所以它会像它们一样被调用:

    list.Merge((l,r)=> l.IsFollowedBy(r), (l,r)=> l.CombineWith(r));
    

    如果你没有这些方法,那么你将不得不这样称呼它:

    list.Merge((l,r)=> l.Category==r.Category && l.To +1 == r.From,
               (l,r)=> new Range(){From = l.From, To=r.To, Category = l.Category});
    

    【讨论】:

    • 不错,通用的解决方案。我唯一的问题是你使用GetEnumerator() 没有using...
    • 为什么这被否决了?它有效,它是通用的,它返回一个很好的 IEnumerable 可实时使用(当合并发生时)。
    • @Timwi:我不确定你在说什么。 IEnumerator 并不暗示 IDisposable
    • @James,看看 Enumerable 类中的所有实现。 (并看一下 IEnumerator 的声明: public interface IEnumerator : IDisposable, IEnumerator)
    • @James, IEnumerator&lt;T&gt; 实现 IDisposable... 查看 MSDN ;) msdn.microsoft.com/en-us/library/78dfe2yb.aspx
    【解决方案4】:

    好吧,从问题的陈述来看,我认为很明显,您无法避免遍历 200 万个项目的原始集合:

    var output = new List<Range>();
    var currentFrom = list[0].From;
    var currentTo = list[0].To;
    var currentCategory = list[0].Category;
    for (int i = 1; i < list.Count; i++)
    {
        var item = list[i];
        if (item.Category == currentCategory && item.From == currentTo + 1)
            currentTo = item.To;
        else
        {
            output.Add(new Range { From = currentFrom, To = currentTo,
                Category = currentCategory });
            currentFrom = item.From;
            currentTo = item.To;
            currentCategory = item.Category;
        }
    }
    output.Add(new Range { From = currentFrom, To = currentTo,
        Category = currentCategory });
    

    我很想看看是否有更优化性能的解决方案。

    编辑:我假设输入列表已排序。如果不是,我建议先对其进行排序,而不是尝试将其放入算法中。排序只需要 O(n log n),但如果你试着把它弄进去,你很容易得到 O(n²),这更糟。

    list.Sort((a, b) => a.From < b.From ? -1 : a.From > b.From ? 1 : 0);
    

    顺便说一句,我写这个解决方案是因为您要求一个性能优化的解决方案。为此,我没有把它做成泛型,没有使用委托,没有使用Linq扩展方法,使用原始类型的局部变量,尽量避免访问对象字段。

    【讨论】:

    • 是的,我需要这个 // 或者你可能需要: // if (item.Category == currentCategory && item.From == currentTo + 1) 我稍微更改了问题描述并添加了如果元素是连续的,我只需要合并的解释。
    • 此方法也有效,并且与下面的通用解决方案答案一样快。两者都以相同的速度产生相同的结果。谢谢。
    • 我在此处提出的所有 4 个答案中运行了包含 200 万个条目的代码,并且所有这些都在完全相同的时间内完成,结果相同,5 秒。真的很抱歉,我只能接受一个答案。
    • 感谢您的解释。我很惊讶它会是同样的速度,但既然你跑了,我相信你。如果真的是同样的速度,那么你接受的答案肯定更好,因为它是可重复使用的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-02
    相关资源
    最近更新 更多