根据条件合并IEnumerable中的元素

时间:2010-08-15 14:19:09

标签: c# .net algorithm collections

我正在寻找一些快速有效的方法来合并数组中的项目。这是我的情景。该集合按From排序。相邻元素不一定相差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项目,我希望尽可能高效。

4 个答案:

答案 0 :(得分:5)

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

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});

答案 1 :(得分:2)

嗯,从问题陈述中我认为很明显你无法避免遍历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扩展方法,我使用了原始类型的局部变量,并试图尽可能避免访问对象字段。

答案 2 :(得分: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);

答案 3 :(得分:0)

假设列表已排序 - 并且 - 范围不重叠,正如您在问题中所述,这将在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--);
        }
    }

编辑:

添加了类别检查,并修复了原位版本。