分割重叠日期范围的最快方法

时间:2011-04-19 06:31:06

标签: c# split overlap date-range

我在SQL DB表中有日期范围数据,这些数据包含以下三个(仅相关)列:

  • ID(int identity)
  • RangeFrom(仅限日期)
  • RangeTo(仅限日期)

对于任何给定的日期范围,可能存在可能重叠(完全或部分)的任意数量的记录。

条件

  1. 具有较高ID(较新记录)的每条记录优先于可能重叠(完全或部分)的旧记录
  2. 范围至少为1天(RangeFromRangeTo相差一天)
  3. 因此,对于给定的日期范围(不超过5年),我必须

    1. 获取属于此范围(全部或部分)的所有范围记录
    2. 将这些重叠分成不重叠的范围
    3. 返回这些新的非重叠范围
    4. 我对它的看法

      由于存在大量与这些范围相关的复杂数据(大量连接等),并且由于处理器+内存功率比SQL数据库引擎效率高得多,因此我决定将重叠数据从DB加载到我的数据层并执行内存中的范围斩波/分裂。这使我在开发和执行方面具有更大的灵活性和速度。

      如果你认为这应该在DB中更好地处理,请告诉我。

      问题

      我想写最快的,如果可能的话也是资源非饥饿的转换算法。由于我获得了大量这些记录并且它们与各种用户相关,因此我必须为每个用户及其重叠范围数据集运行此算法。

      分割这些重叠范围的最有效(快速且非资源饥饿)方式是什么?

      示例数据

      我有ID=1ID=5的记录以这种方式在视觉上重叠(日期实际上是无关紧要的,我可以用这种方式更好地显示这些重叠):

             6666666666666
                      44444444444444444444444444         5555555555
                2222222222222            333333333333333333333            7777777
      11111111111111111111111111111111111111111111111111111111111111111111
      

      结果应如下所示:

      111111166666666666664444444444444444444444333333333555555555511111117777777
      

      结果实际上看起来好像我们正在从顶部查看这些重叠,然后获取我们从这个自上而下的视图中看到的ID。

      结果实际上会转换为新的范围记录,因此旧ID变得无关紧要。但是将使用他们的RangeFromRangeTo值(以及所有相关数据):

      111111122222222222223333333333333333333333444444444555555555566666667777777
      

      这当然只是重叠范围的一个例子。对于任何给定的日期范围,它可以是从0记录到X的任何内容。而且我们可以看到范围ID = 2被4和6完全覆盖,所以它变得完全过时了。

4 个答案:

答案 0 :(得分:4)

可以为空的整数数组

我想出了自己的想法:

  1. 对于给定的日期范围,我将创建一个整数的内存数组,其中的项目数与该范围内的天数一样多。

  2. 使用null值填充数组。所有这些。

  3. 按ID以相反顺序订购记录

  4. 通过迭代有序记录来展平重叠范围,并对每个项目执行以下操作:

    1. 获取项目
    2. 计算数组的开始和结束偏移量(天差)
    3. 将这两个偏移之间的所有数组值设置为项ID,但仅在值为null
    4. 时设置
    5. 继续执行步骤4.1
  5. 您最终获得了一系列展平范围并填充了记录ID

  6. 创建新的记录集,并在数组中的ID更改时创建每个新记录。每条记录应使用与数组

  7. 中设置的记录ID相关联的数据
  8. 为下一个人及其重叠范围集重复整个事情(不要忘记重复使用相同的数组)。 =回到第2步。

  9. 基本上就是这样。

    给定日期范围的10年需要大约一个数组。 3650个可以为空的整数,我认为这是一个相当小的内存占用(每个整数占用4个字节,但我不知道有多少空间占用一个可以为intbool的可空整数,但我们假设8总计在3650 * 8 = 28.52k的字节,并且可以在内存中轻松快速地操作。由于我没有保存日期范围,因此拆分或类似的任何操作几乎不仅仅是赋值操作,如果检查是否已经设置了值。

    10年的日期范围是一种罕见的夸张极端。 75%的日期范围将在3个月或一年中的四分之一(90天* 8字节= 720字节),99%将在一年的范围内(365 * 8 = 2920字节= 2,85k)< / p>

    我觉得这个算法不适合于展平重叠的日期范围。

    对于内存占用量的一半,我可以使用int代替int?并设置为-1而不是null

    过早的迭代循环中断可能性

    我还可以保留未设置的天数,当它达到0时,我可以很容易地打破循环,因为所有剩余的范围都是完全重叠的,因此它们不会在数组中设置更多的值。因此,当我有很多范围记录时,这甚至可以加快速度(这将是相当罕见的)。

答案 1 :(得分:2)

免费Time Period Library for .NET包含工具 TimePeriodIntersector ,它与各种重叠时间范围相交。

该算法使用时间轴并枚举一个时间范围内的所有时刻(计算每时刻的起点/终点):

// ----------------------------------------------------------------------
public void TimePeriodIntersectorSample()
{
  TimePeriodCollection periods = new TimePeriodCollection();

  periods.Add( new TimeRange( new DateTime( 2011, 3, 01 ), new DateTime( 2011, 3, 10 ) ) );
  periods.Add( new TimeRange( new DateTime( 2011, 3, 05 ), new DateTime( 2011, 3, 15 ) ) );
  periods.Add( new TimeRange( new DateTime( 2011, 3, 12 ), new DateTime( 2011, 3, 18 ) ) );

  periods.Add( new TimeRange( new DateTime( 2011, 3, 20 ), new DateTime( 2011, 3, 24 ) ) );
  periods.Add( new TimeRange( new DateTime( 2011, 3, 22 ), new DateTime( 2011, 3, 28 ) ) );
  periods.Add( new TimeRange( new DateTime( 2011, 3, 24 ), new DateTime( 2011, 3, 26 ) ) );

  TimePeriodIntersector<TimeRange> periodIntersector =
                    new TimePeriodIntersector<TimeRange>();
  // calculate intersection periods; do not combine the resulting time periods
  ITimePeriodCollection intersectedPeriods = periodIntersector.IntersectPeriods( periods, false );

  foreach ( ITimePeriod intersectedPeriod in intersectedPeriods )
  {
    Console.WriteLine( "Intersected Period: " + intersectedPeriod );
  }
  // > Intersected Period: 05.03.2011 - 10.03.2011 | 5.00:00
  // > Intersected Period: 12.03.2011 - 15.03.2011 | 3.00:00
  // > Intersected Period: 22.03.2011 - 24.03.2011 | 2.00:00
  // > Intersected Period: 24.03.2011 - 26.03.2011 | 2.00:00
} // TimePeriodIntersectorSample

ID映射应该是一项简单的任务。

答案 2 :(得分:1)

我不太确定会有多么有用,但我接近这个的方式...... (首先未经优化以便于理解......)

  • 将表格映射从[ID-&gt; range]转换为[date-&gt; ID列表]。

(按日期排序,每个日期 - 无论是开始还是结束,都是到达下一个日期的时间范围的开始。) 这样你的桌子就像:

        |666|666666|6666|
        |   |      |4444|444|444444444444|4444444|         |55555|55555|
        |   |222222|2222|222|            |3333333|333333333|33333|     |       |7777777
 1111111|111|111111|1111|111|111111111111|1111111|111111111|11111|11111|1111111|

 1234567|890|123456|7890|123|4


 1 -> 1
 8 -> 1,6
 11 -> 6,2,1
 17 -> 6,4,2,1
 21 -> 4,2,1
 24 -> 4,1
 ...
  • 选择每个列表中的最大元素
  • 连接具有相同最大值的记录。

由于您的最终数据库中将有重复的ID(在您的示例中,“1”将分为两个段),因此最终将数据库保持为date-&gt; ID格式而不是ID-&gt;范围

现在进行明显的优化 - 当然不要在每个日期记录中保留ID列表。只需使用空ID填写date-&gt; ID表,并在填写最终记录时,替换目前为止找到的最大值记录:

  • 创建所有日期条目的表格,[日期 - &gt; ID]
  • 表示原始表中的每条记录:
    • 在-to,
    • 范围内选择日期
    • 如果ID值为null或低于当前检查的记录ID,请填写当前ID。
  • 然后连接 - 如果下一条记录与之前的ID相同,则删除下一条。
  • 最后,你可能想要对一个比特进行非规范化,用[date - &gt;替换为一个范围提取两个连续记录。 ID,长度]或[日期 - &gt; ID日期,结束日期]

添加新记录就像创建操作的一次迭代。另一方面,删除记录似乎非常棘手。

答案 3 :(得分:0)

实际上,您希望堆叠数据,并从堆栈中选择最大值。我不得不在之前和我们使用的方法中实现类似的东西,这给了我们比你需要的更多的灵活性,所以可能不适合这样做:

拥有一个用于管理记录的对象,并将每条记录添加到此对象。添加记录时,创建新的日期范围并将记录的值与范围相关联。然后检查范围是否与任何其他现有范围重叠。如果它确实重叠,则为每个重叠创建一个新范围并关联两个/所有的所有值(取决于在添加每个范围时是否这样做,或在单个过程中)与新范围重叠的范围。这可以在添加数据时完成,也可以在添加所有数据后一次性完成。

最后你有一个包含唯一范围的对象,每个范围都有一组与之关联的值,有点像你上面的图片。

       |666|666666|6666|
       |   |      |4444|444|444444444444|4444444|         |55555|55555|
       |   |222222|2222|222|            |3333333|333333333|33333|     |       |7777777
1111111|111|111111|1111|111|111111111111|1111111|111111111|11111|11111|1111111|

然后,您可以提供一个具有展平函数的类(可能使用策略模式),它将具有值集合的唯一范围转换为具有单个值的唯一范围,这显然会连接最终具有相同值的范围

您需要一个从每个唯一范围中选择最大值的类,但您可能还需要选择最小值,对值求和,平均值,计算它们等等。这些选项中的每一个都可以通过传递来完成战略的不同实施。

正如我所说的那样,这种方法可能效率低于仅选择最大值的方法,因为在这种情况下你不需要保留堆栈中的所有值,但是我记得实现是相当直接的