最近被人問到這樣一個問題的解決方案:在一個餐館的預約系統中,接受用戶在將來任意一段時間內的預訂用餐,用戶在預訂的時候須要提供用餐的開始時間和結束,餐館的餐桌是用限的,問題是,系統要在最快的時間段計算出在該用戶預約的時間段內是否還有可用的餐桌?其實相似的問題咱們在作系統時常常碰到,好比在一個「任務管理」系統中,咱們要知道某個任務的執行時間段是否跟已知的時間段有重疊,揭開這些特定需求的外表,本質的問題能夠這樣描述:在一個線性的空間中,已存在不少區間段分佈在該線性空間中,現給出一個指定的區間段,求出空間中全部和該區間段有重疊的空間段集合。node
怎樣定義「兩個區間重疊」?你們都能馬上判斷出這個結果,可是咱們要用語言定義出來,或者用數學公式表達出來才能創建解決模型。先看下面一張圖:
咱們把上面的區間叫作t1,下面的區間叫作t2,根據上圖能夠看出,區間t2和區間t1有重疊的話,必然要知足下列三種狀況之一:python
若是咱們用數學公式表達的話,就是:
算法
\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
Merge Index
利用兩個索引字段。Redis中也有集合的交集運算實現
ZINTERSTORE
。這種方式從直觀感受上比窮舉法好像快不少。咱們能夠大概計算評估下:第一步是要從兩個集合中範圍查找子集,採用通常的
樹結構
,都能作到
\(\theta(\log{N})\),第二步要作兩個子集的交集運算,複雜度又回到了
\(\theta(N)\)。這其實和上面的窮舉法感受沒有什麼區別。
其實各類各樣的樹結構
,都是利用二分原理快速找到須要的數據,其複雜度都是 \(\theta(\log{N})\)級。IntervalTree也是利用這一特性,把每一個區間二分對摺,淘汰掉另一半來快速找到所要區間數據。
code
構建一個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的取值方式使樹更平衡些。留言下說你的思考,謝謝!