区间重叠计算及IntervalTree初识

最近被人问到这样一个问题的解决方案:在一个餐馆的预约系统中,接受用户在将来任意一段时间内的预订用餐,用户在预订的时候须要提供用餐的开始时间和结束,餐馆的餐桌是用限的,问题是,系统要在最快的时间段计算出在该用户预约的时间段内是否还有可用的餐桌?其实相似的问题咱们在作系统时常常碰到,好比在一个“任务管理”系统中,咱们要知道某个任务的执行时间段是否跟已知的时间段有重叠,揭开这些特定需求的外表,本质的问题能够这样描述:在一个线性的空间中,已存在不少区间段分布在该线性空间中,现给出一个指定的区间段,求出空间中全部和该区间段有重叠的空间段集合。node

定义区间重叠

怎样定义“两个区间重叠”?你们都能马上判断出这个结果,可是咱们要用语言定义出来,或者用数学公式表达出来才能创建解决模型。先看下面一张图:
区间重叠
咱们把上面的区间叫作t1,下面的区间叫作t2,根据上图能够看出,区间t2和区间t1有重叠的话,必然要知足下列三种状况之一:python

  1. t2的开始时间落在t1区间段内
  2. t2的结束时间落在t1区间段内
  3. t2直接包含了整个t1区间

若是咱们用数学公式表达的话,就是:
算法


\begin{equation}
t2_{starttime} <= t1_{endtime} \quad and \quad t2_{endtime} >= t1_{starttime}
\end{equation}
数据库

穷举法

根据上面的公式,穷举全部区间集合中的元素,逐个计算,两两比较,返回全部知足要求的区间元素。时间计算复杂度是 \(\theta(N)\)数据结构

交集

根据上面的公式1,能够构建两个有序集合,分别存放全部区间段的开始时间和结束时间,假设两个集合分别是 S 和 E,则查询和指定区间(s,e)重叠的全部区间能够这样计算:先计算集合S中全部小于e的元素,再计算出集合E中全部大于s的元素,计算出这两个结果的交集,则为最终结果。用公式表达就是:
app


\begin{equation}
{x|x \in S \land x \leq e } \cap {y|y \in E \land y \geq s }
\end{equation}
spa


在具体系统开发中,实现方式有多种。若是基于数据库,如MySQL,能够直接经过 Merge Index利用两个索引字段。Redis中也有集合的交集运算实现 ZINTERSTORE。这种方式从直观感受上比穷举法好像快不少。咱们能够大概计算评估下:第一步是要从两个集合中范围查找子集,采用通常的 树结构,都能作到 \(\theta(\log{N})\),第二步要作两个子集的交集运算,复杂度又回到了 \(\theta(N)\)。这其实和上面的穷举法感受没有什么区别。

初识IntervalTree算法

其实各类各样的树结构,都是利用二分原理快速找到须要的数据,其复杂度都是 \(\theta(\log{N})\)级。IntervalTree也是利用这一特性,把每一个区间二分对折,淘汰掉另一半来快速找到所要区间数据。
图2code

构建

构建一个IntervalTree很简单,每次添加一个区间元素t时,先比较区间t是否覆盖x_center(x_center就是当前整个区间的中间点,从算法效率上来说,不该该是区间起点和终点的平均值,而应该是落中这个区间内全部元素的中位值)值,若是覆盖则把区间的开始值和结束值分别存放在该节点的两个有序集合中,分别是全部覆盖区间的开始时间集合和结束时间集合。若是区间t在x_center以后,则放到右子节点上,处理方式同样(递归处理);若是区间t在x_center以前,则放到左子点上,也是递归处理。这样每一个节点的数据结构大概这样:blog

class Node(object):
    def __init__(self, boundary):
        # 区间范围
        self.boundary = boundary
        # 中间值
        self.x_center = (boundary[1] - boundary[0]) / 2 + boundary[0]
        # 左子节点,该节点下的全部区间都小于x_center
        self.left = None
        # 右子节点,该节点下的全部区间都大于x_center
        self.right = None
        # 覆盖x_center的全部节点的开始时间集合
        self.begins = []
        # 覆盖x_center的全部节点的结束时间集合
        self.ends = []

    def add_overlap_interval(self, start_point, end_point):
        self.begins.append(start_point)
        self.begins = sorted(self.begins)
        self.ends.append(end_point)
        self.ends =  sorted(self.ends)

boundary参数表左该节点所能影响到整个区间范围,包含了一个起点和终点。这里简单的把x_center值取成范围的中间值。left 和 right 分别为左子节点和右子节点。begins为有序集合,里面的元素为全部知足特定条件(覆盖x_center)的区间的开始值。同begins同样,ends存放的是全部覆盖x_center的区间的结束值的有序集合。方法add_overlap_interval的做用就是添加能覆盖x_center的间到此节点中。排序

有了上面描述的节点定义,IntervalTree就是由上述节点组成的,即然是树结构,因此就有根节点的概念。每一个IntervalTree有一个根节点。

class IntervalTree(object):
    def __init__(self, min_point, max_point):
        self.min_point = min_point
        self.max_point = max_point
        self.root = Node((min_point, max_point))

    def add(self, start_point, end_point):
        node = self.root
        while end_point < node.x_center or start_point > node.x_center:
        # 若是区间没有覆盖x_center,则添加到子节点中去
            if end_point < node.x_center:
            # 添加到左子节点
                if not node.left:
                    node.left = Node((node.boundary[0], node.x_center))
                node = node.left
            else:
            # 添加到右子节点
                if not node.right:
                    node.right = Node((node.x_center, node.boundary[1]))
                node = node.right
        else:
        # 区间覆盖x_center,则添加到此节点
            node.add_overlap_interval(start_point, end_point)

查询

对于一个区间集合 S,对于给定的区间 q,现要查询出全部和区间 q 有重叠的区间子集合,怎样作呢?根据前面的区间重叠定义中说的,若是一个区间的开始时间或者结束时间落在了另一个区间内,或者彻底包含这个区间,则是重叠的。因此咱们按照这个思路分别求解。

先查出全部点(不管开始时间或结束时间点)落在查询区间 q 段内的数据。这点很好作,能够把全部开始时间和结束时间放在一个排序的数据结构中(如红黑树),这样求解就转换成了在一个树中求范围数据,其复杂度是 \(\theta(\log{N})\)

再找出那些区间彻底包含了查询区间q的数据。这里有个技巧能够利用,在区间q中随便取一个点p,咱们能够有以下结论推理:凡是区间能覆盖到点p的,则确定和区间q有重叠。这个用数学公式很好推理出来。因此如今的问题就是在一个IntervalTree树中查出给定一个点的全部覆盖区间子集合。这个问题的求解和构建树结构一致。从根节点开始查询,查询此节点中全部可覆盖的区间。而后根据指定点落在左,或右子节点上来2分查找,直到没有没有子节点时退出。这里要注意一点:若是指定点恰好等于x_center点,则当即中止查找子节点,并返回当前节点所包含的全部区间数据。查找算法以下:

def search_intervals(self, point):
        # 从根节点开始查找
        node = self.root
        result = []
        while point != node.x_center:
        # 若是查找点没有和x_center相同
            if point < node.x_center:
            # 若是查找点在x_center前边,则该节点内全部的区间中,开始时间早于或者等于point的区间都是覆盖point的
                result += [s for s in node.begins if s <= point]
                node = node.left
            else:
            # 若是查找点在x_center后边,则该节点内全部的区间中,结束时间晚于或者等于point的区间都是覆盖point的
                result += [s for s in node.ends if s >= point]
                node = node.right
            if not node:
                break
        else:
            result += node.begins
        return result

至此,整个IntervalTree的大概思路表述完了。上面的代码其实更多的是讲述思路,细节没有注意,好比Node结构中begins和ends用LinkedList仍是RBTree更合适。还有其它一些思考,好比区间的删除,以及具体数据业务场景中,选择什么样的x_center的取值方式使树更平衡些。留言下说你的思考,谢谢!

参考:wiki_IntervalTree