合并重叠的时间间隔?

时间:2012-07-14 00:44:06

标签: c# linq

我有以下内容:

public class Interval
{
   DateTime Start;
   DateTime End; 
}

我有一个包含多个间隔的List<Interval>对象。我试图实现以下目标(我使用数字使其易于理解):

[(1, 5), (2, 4), (3, 6)] --->  [(1,6)]
[(1, 3), (2, 4), (5, 8)] --->  [(1, 4), (5,8)]

我目前在Python中这样做:

def merge(times):
    saved = list(times[0])
    for st, en in sorted([sorted(t) for t in times]):
        if st <= saved[1]:
            saved[1] = max(saved[1], en)
        else:
            yield tuple(saved)
            saved[0] = st
            saved[1] = en
    yield tuple(saved)

但是我试图在C#中实现相同的目标(LINQ将是最好的但是可选的)。有关如何有效地做到这一点的任何建议?

5 个答案:

答案 0 :(得分:11)

这是使用yield return的版本 - 我发现它比执行Aggregate查询更容易阅读,尽管它仍然是懒惰评估的。这假设您已经订购了列表,如果没有,只需添加该步骤。

IEnumerable<Interval> MergeOverlappingIntervals(IEnumerable<Interval> intervals)
{
  var accumulator = intervals.First();  
  intervals = intervals.Skip(1);

  foreach(var interval in intervals)
  {
    if ( interval.Start <= accumulator.End )
    {
        accumulator = Combine(accumulator, interval);
    }
    else
    {
        yield return accumulator;
        accumulator = interval;     
    }       
  }

  yield return accumulator;
}

Interval  Combine(Interval start, Interval end)
{
  return new Interval 
  {
    Start = start.Start,
    End = Max(start.End, end.End),
  };
}

private static DateTime Max(DateTime left, DateTime right) 
{
    return (left > right) ? left : right;
}

答案 1 :(得分:3)

这可能不是最漂亮的解决方案,但也可以起作用

public static List<Interval> Merge(List<Interval> intervals)
{
    var mergedIntervals = new List<Interval>();
    var orderedIntervals = intervals.OrderBy<Interval, DateTime>(x => x.Start).ToList<Interval>();

    DateTime start = orderedIntervals.First().Start;
    DateTime end = orderedIntervals.First().End;

    Interval currentInterval;
    for (int i = 1; i < orderedIntervals.Count; i++)
    {
        currentInterval = orderedIntervals[i];

        if (currentInterval.Start < end)
        {
            end = currentInterval.End;
        }
        else
        {
            mergedIntervals.Add(new Interval()
            {
                Start = start,
                End = end
            });

            start = currentInterval.Start;
            end = currentInterval.End;
        }
    }

    mergedIntervals.Add(new Interval()
                {
                    Start = start,
                    End = end
                });

    return mergedIntervals;
}

我们将不胜感激。

此致

答案 2 :(得分:2)

我被#34;未在这里创造&#34;今晚是综合症,所以这里是我的。使用枚举器直接为我保存了几行代码,使其更清晰(IMO),并处理没有记录的情况。我想如果你关心它,它可能会更快地运行...

public IEnumerable<Tuple<DateTime, DateTime>> Merge(IEnumerable<Tuple<DateTime, DateTime>> ranges)
{
    DateTime extentStart, extentEnd;
    using (var enumerator = ranges.OrderBy(r => r.Item1).GetEnumerator()) {
        bool recordsRemain = enumerator.MoveNext();
        while (recordsRemain)
        {
            extentStart = enumerator.Current.Item1;
            extentEnd = enumerator.Current.Item2;
            while ((recordsRemain = enumerator.MoveNext()) && enumerator.Current.Item1 < extentEnd)
            {
                if (enumerator.Current.Item2 > extentEnd)
                {
                    extentEnd = enumerator.Current.Item2;
                }
            }
            yield return Tuple.Create(extentStart, extentEnd);
        }
    }
}

在我自己的实现中,我使用TimeRange类型来存储每个Tuple<DateTime, DateTime>,就像其他人一样。我并没有把它包括在内,只是为了保持专注/主题。

答案 3 :(得分:1)

这种合并通常被视为功能语言的折叠。 LINQ等价物是Aggregate

IEnumerable<Interval<T>> Merge<T>(IEnumerable<Interval<T>> intervals) 
  where T : IComparable<T>
{
    //error check parameters
    var ret = new List<Interval<T>>(intervals);
    int lastCount
    do
    {
        lastCount = ret.Count;
        ret = ret.Aggregate(new List<Interval<T>>(),
                    (agg, cur) =>
                    {
                        for (int i = 0; i < agg.Count; i++)
                        {
                            var a = agg[i];
                            if (a.Contains(cur.Start))
                            {
                                if (a.End.CompareTo(cur.End) <= 0)
                                {
                                    agg[i] = new Interval<T>(a.Start, cur.End);
                                }
                                return agg;
                            }
                            else if (a.Contains(cur.End))
                            {
                                if (a.Start.CompareTo(cur.Start) >= 0)
                                {
                                    agg[i] = new Interval<T>(cur.Start, a.End);
                                }
                                return agg;
                            }
                        }
                        agg.Add(cur);
                        return agg;
                    });
    } while (ret.Count != lastCount);
    return ret;
}

我将Interval类设为泛型(Interval<T> where T : IComparable<T>),添加了bool Contains(T value)方法,并使其成为不可变的,但是如果你想使用类定义,则不需要更改它现在就拥有它。

答案 4 :(得分:0)

我使用TimeRange作为存储范围的容器:

public class TimeRange
{
    public TimeRange(DateTime s, DateTime e) { start = s;  end = e; }

    public DateTime start;
    public DateTime end;
}

它将组合两个时间范围的问题分开。因此,当前时间范围(工作)与先前合并的时间范围匹配。如果先前添加的时间范围之一已过时,则将其删除并使用新的时间范围(从工作和匹配的时间范围组合)。 我想出的两个范围()和[]的情况如下:

  1. []()
  2. ([])
  3. [(])
  4. [()]
  5. ([)]
  6. ()[]

    public static IEnumerable<TimeRange> Merge(IEnumerable<TimeRange> timeRanges)
    {
        List<TimeRange> mergedData = new List<TimeRange>();
    
        foreach (var work in timeRanges)
        {
            Debug.Assert(work.start <= work.end, "start date has to be smaller or equal to end date to be a valid TimeRange");
            var tr = new TimeRange(work.start, work.end);
    
            int idx = -1;
            for (int i = 0; i < mergedData.Count; i++)
            {
                if (tr.start < mergedData[i].start)
                {
                    if (tr.end < mergedData[i].start)
                        continue;
                    if (tr.end < mergedData[i].end)
                        tr.end = mergedData[i].end;
                }
                else if (tr.start < mergedData[i].end)
                {
                    tr.start = mergedData[i].start;
    
                    if (tr.end < mergedData[i].end)
                        tr.end = mergedData[i].end;
                }
                else
                    continue;
    
                idx = i;
                mergedData.RemoveAt(i);
                i--;
            }
    
            if (idx < 0)
                idx = mergedData.Count;
    
            mergedData.Insert(idx, tr);
        }
    
        return mergedData;
    }