Linq:获取相交的项目

时间:2012-12-17 13:30:31

标签: c# .net linq

我是新手,我有点麻烦:

我有timeitems的列表:

06:40 - 07:10
06:55 - 07:13
07:00 - 08:35
07:13 - 07:14
09:00 - 10:00
10:00 - 11:00
12:00 - 13:00
12:30 - 14:00

现在我想要所有相交的项目:

06:40 - 07:10
06:55 - 07:13
07:00 - 08:35
07:13 - 07:14

12:00 - 13:00
12:30 - 14:00


var intersects = timeitems
            .Where(a => timeitems
            .Any(b => Utilities.IsBetween(a.SpanRangeStartIndex, b.SpanRangeStartIndex, b.SpanRangeEndIndex)))
            .AsParallel()
            .ToList();

但我只能得到这个,我不知道为什么:

06:55 - 07:13
07:00 - 08:35
07:13 - 07:14

12:30 - 14:00

非常感谢你的帮助(记住,我是.net: - 的新手) -

编辑*

好的,timeitem只是一个包含两个属性的项目列表:

Item1(SpanRangeStartIndex = 06:40 SpanRangeEndIndex = 07:10)

Item2(SpanRangeStartIndex = 06:55 SpanRangeEndIndex = 07:13)

...

Utilities.IsBetween检查值是否介于两个其他值之间(如果3介于2和6之间 - > true)

    public static bool IsBetween(int value, int start, int end)
    {
        return (value > start) & (value <end);
    }

抱歉我的英语不好而且不好c#-skill ......我对此很新。

感谢

4 个答案:

答案 0 :(得分:1)

欢迎来到SO!

我认为您要解决的问题是,您想知道您的范围集中的哪些范围与同一组中的任何其他范围重叠。

问题似乎是你测试范围的一端是“之间”而不是另一端。 (我编写了一个示例程序,它可以完成您的工作并添加一些注释,并从属性名称和.AsParallel()调用中删除'SpanRange'和'Index' - 这可能会更改返回的数据的顺序,但仍然有相同的整体内容。)

var intersects = 
    data.Where(a => data
        .Any(b => 
            IsBetween(a.Start, b.Start, b.End) // <-- this is the test you did
            || IsBetween(a.End, b.Start, b.End) // <-- the missing other end
//          || IsBetween(b.Start, a.Start, a.End) // potentially necessary
//          || IsBetween(b.End, a.Start, a.End) // potentially necessary
        ));

我添加了另外两个评论IsBetween次调用,因为我认为当一个范围完全包含在另一个范围内时,可能无法显示“完全包含”的范围测试。

另一方面,我可能会尝试通过首先考虑两个范围不相交的简单情况来改变你对范围相交的测试方法。

两个范围在以下两个范围内都不相交:

  1. rangeA.End < rangeB.Start表示:rangeA完全位于'rangeB
  2. 的左侧
  3. rangeA.Start > rangeB.End说:rangeA完全位于'rangeB
  4. 的右侧

    doNotIntersect = (rangeA.End < rangeB.Start) || (rangeA.Start > rangeB.End)

    因此,我们可以通过否定上述表达式来测试范围是否相交:
    isIntersecting = (rangeA.End >= rangeB.Start) && (rangeA.Start <= rangeB.End)

    但是,我注意到你的测试之间没有使用“&gt; =”或“&lt; =”所以只与另一个开头共享一个结尾的范围不相交。因此,样本中的09:00 - 10:00范围不会与样本中的10:00 - 11:00范围重叠。所以,你很可能会使用>&amp; <而不是>=&amp; <=运营商。

    如果您需要,我很乐意发布代码和结果。

答案 1 :(得分:0)

您会看到此问题,因为您只获得“此项目在其他项目中启动的项目”,并且不包括“此项目期间其他项目启动的项目”。

一个简单的解决方法是

var intersects = timeitems
    .Where(a => timeitems.Any(b => 
        Utilities.IsBetween(a.SpanRangeStartIndex,
            b.SpanRangeStartIndex, b.SpanRangeEndIndex) ||
        Utilities.IsBetween(b.SpanRangeStartIndex,
            a.SpanRangeStartIndex, a.SpanRangeEndIndex)))
    .AsParallel()
    .ToList();

会使您的代码对称,并且会包含缺少的06:40 - 07:1012:00 - 13:00

然而,这(与你原来的一样)是非常低效的 - O(n ^ 2),当O(n)算法应该是可能的时候。

答案 2 :(得分:0)

考虑何时处理从12:3014:00

的时间

前面的元素(从12:0013:00)与该窗口相交,但是您的查询错过了它,因为您只是检查 start 时间是否在当您必须检查结束时间是否在范围内时的范围。

也就是说,您可以将查询更改为此(删除了AsParallelToList方法,因为它们不是解决方案的组成部分):

var intersects = timeitems
    .Where(a => timeitems
        .Any(b => 
            // Check the start of the window...
            Utilities.IsBetween(a.SpanRangeStartIndex, 
                b.SpanRangeStartIndex, b.SpanRangeEndIndex) &&
            // *AND* the end of the window...
            Utilities.IsBetween(a.SpanRangeEndIndex, 
                b.SpanRangeStartIndex, b.SpanRangeEndIndex)));

现在,您正在遍历每个项目的整个 timeItems序列,即使您知道已经匹配和交叉的项目(因为你没有配对它们,你不需要说项目a与项目b重叠,你只需返回重叠项目。“

有了这个,您可以通过不使用LINQ来减少必须迭代N ^ 2个项目,但只有在您的集合具体化并实现IList<T> interface时,才能减少这些数组和List<T>个实例)。

你会向前看,跟踪重叠和产生的结果,如下所示:

public IEnumerable<TimeItem> GetOverlappingItems(this IList<TimeItem> source)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");

    // The indexes to ignore that have been yielded.
    var yielded = new HashSet<int>();

    // Iterate using indexer.
    for (int index = 0; index < source.Count; ++index)
    {
        // If the index is in the hash set then skip.
        if (yielded.Contains(index)) continue;

        // Did the look ahead yield anything?
        bool lookAheadYielded = false;

        // The item.
        TimeItem item = source[index];

        // Cycle through the rest of the indexes which are
        // not in the hashset.
        for (int lookAhead = index + 1; lookAhead < source.Count; ++lookAhead)
        {
            // If the item has been yielded, skip.
            if (yielded.Contains(lookAhead)) continue;

            // Get the other time item.
            TimeItem other = source[lookAhead];

            // Compare the two.  See if the start or the end
            // is between the look ahead.
            if (Utilities.IsBetween(item.SpanRangeStartIndex,
                    other.SpanRangeStartIndex, other.SpanRangeEndIndex) ||
                Utilities.IsBetween(item.SpanRangeEndIndex,
                    other.SpanRangeStartIndex, other.SpanRangeEndIndex))
            {
                // This is going to be yielded.
                lookAheadYielded = true;

                // Yield the item.
                yield return other;

                // Add the index to the hashset of what was yielded.
                yielded.Add(lookAhead);
            }
        }

        // Was a look ahead yielded?
        // No need to store the index, we're only moving
        // forward and this index doesn't matter anymore.
        if (lookAheadYielded) yield return item;
    }
}

答案 3 :(得分:0)

LINQ可能不是一个好主意,因为你正在进行大量的重复计算。如果你可以假设它们都是按起始索引排序的(你可以使用LINQ来命令它,如果你不能保证的那样)那么在迭代它们时保持滚动窗口要容易得多:

timeitem workingRange = null, rangeStart = null;
bool matched = false;
foreach(timeitem t in timeitems) // timeitems.OrderBy(ti => ti.SpanRangeStartIndex) if unsorted
{
    if(workingRange is null)
    {
        rangeStart = t;
        workingRange = new timeitem { SpanRangeStartIndex = t.SpanRangeStartIndex, SpanRangeEndIndex = t.SpanRangeEndIndex };
        continue;
    }

    if(Utilities.IsBetween(t.SpanRangeStartIndex,
        workingRange.SpanRangeStartIndex, workingRange.SpanRangeEndIndex))
    {
        if(!matched)
        {
            matched = true;
            yield return rangeStart;
        }
        workingRange.SpanRangeEndIndex = Math.Max(workingRange.SpanRangeEndIndex, t.SpanRangeEndIndex);
        yield return t;
    }
    else
    {
        matched = false;
        rangeStart = t
        workingRange = new timeitem { SpanRangeStartIndex = t.SpanRangeStartIndex, SpanRangeEndIndex = t.SpanRangeEndIndex };
    }
}

一些笔记。保持对范围的原始第一项的引用,因为我不知道它是否是结构/类,并且除非您执行某种转换,否则最好生成原始项。可以轻松修改工作范围以使用DateTime(可能更容易阅读/理解)。我们需要跟踪我们是否匹配,因为我们仍然需要产生/返回原始工作项并确保我们不再产生它(不能使用范围作为度量,因为后续{{1} } s可以完全在初始范围内)。最后,如果我们检查的项目不在范围内,我们会重置所有状态变量并将它们视为我们的起始范围。

这确保您只需要遍历集合一次,而不是事先对其进行排序(如果您可以确保他们在第一时间排除这一点,则无论如何都要消除此需求)。希望有所帮助,希望有一个更简单的方法。