範圍搜索
Author: Subhash Suri
譯者:Koala++ / 屈偉
引
前一陣把搜索引擎的RangeQuery的邏輯重新寫了一遍,我寫的時候就感覺很不對勁,我們的搜索引擎採用的是一種非常怪異的實現,至少我沒在別的搜索引擎裏見過,或是在資料中看到過。我要解決的是二維座標查詢,比如你想知道你周圍五公里內的醫院在什麼地方,蠻力解決方法就是把所有醫院座標得到,把x座標循環過濾一遍,再把y座標循環過濾一遍。其實這還好,因爲一個城市一共也沒多少醫院,但如果調用方把座標查詢寫前面,也就是先過濾x和y座標,再過濾醫院,那就悲劇了。
簡單點的辦法就是把x和y座標有序地保存,那用二分查找定位到x-2.5km, x+2.5km, y-2.5km, y+2.5km,然後取x-2.5km到x+2.5km的posting list和y-2.5km到y+2.5km的posting list做and操作就可以了。
但是還能不能再快呢?這個問題我想了想也沒什麼頭緒,偶然發現了RTree這個數據結構,我感覺這纔是正道。
下面是Range Searching的翻譯。原文地址:http://www.cs.ucsb.edu/~suri/cs235/RangeSearching.pdf
Range Search
我們這裏討論Range Search是希望能找到一個好的數據結構,它能高效地對對象(點,矩形,多邊形)的集合進行範圍查詢。
我們要做的是根據對象的類型和查詢的類型,來尋找一個能在多種應用場景下使用的基本數據結構。
時間-空間的平衡:我們在預處理和存儲上耗費地越多,那我們就可以更快地完成一次查詢。
這裏主要考慮使用(近似)線性空間的數據結構。
Orthogonal Range Search
對於一個有n個點的集合P。它有2n個子集。對於一個幾何的查詢,它會有多少個可能的結果呢?
效率能提高的原因是在查詢結果中只會有子集中的一部分。
正交範圍搜索只處理與座標軸平行的矩形空間中的點集合。(譯註,也就是不能用查詢像圓形,五角形這樣的範圍)。
接下來,我們先討論一維空間中的排序和搜索問題,然後將一維空間的數據結構推廣到多維空間。
1-Dimensional Search
令一維空間的點集合爲P={p1, p2, ...,pn}。
查詢是一個區間:
如果這個區間包含k個點,我們想在時間複雜度爲O(log n + k)的情況下解決這個問題。
Hashing可以做到嗎?爲什麼不能?(譯註:Hash的查詢時間複雜度爲O(1+a),a是負載因子,hash在查詢時只能依次查找,它的時間複雜度是O(n+an))。
一個排序後的數組可以得到這個時間複雜度邊界,但是它無法推廣到多維空間。
我們採用一種替代方案,用一個二叉平衡樹。
Tree Search
在一個排序後的點(key)數組上建立一個平衡二叉樹。
葉子結點對應的是點,中間結點是分支結點。
給定一個區間[xl0,xhi],在樹上搜索xl0和xhi。
搜索得到的兩個葉子結點中的葉子結點就是搜索的結果。
樹搜索部分的時間複雜度是2log n,將葉子結點放入結果集的時間複雜度爲O(k),這裏假設葉子結點是被鏈在一起的。
Canonical subsets
S1, S1... Sk是Canonical子集,Si屬於P。如果範圍查詢的結果可以寫成幾個Si的並集。(譯註:canonical子集的解釋http://en.wikipedia.org/wiki/Canonical,不必深究,就認爲是子集就行了)。
Canonical子集有可能會重疊。
鍵(Key)是用來確定正確的Si,和高效地確定對於一個給定的查詢,使用哪個Si。
在一維空間問題上,樹中每一個結點都有一個Canonical子集:Sv是以v的根結點的子樹的Canonical子集,它其中的元素是這個子樹所有根結點中的點。
1D Range Query
給定查詢[xl0,xhi],查詢樹中滿足u>= xl0的最左葉子結點,和滿足v>= xhi的最左葉子結點。
所有在u和v之間的葉子結點都在所查詢範圍之內。
如果u= xl0或是v= xhi,那麼u, v的canonical子集也包含在範圍中。
將[u,v)區間範圍內的所有其它點由區間中的最大子樹決定。(譯註:陰影中的子樹)。
Query Processing
令z爲從root到u,v搜索路徑上的最後一個普通結點。
先看從z到u的最左邊路徑,當路徑向左時,將右子樹的canonical子集加入結果集。
從z到v的右邊路徑中,當路徑向右時,將左子樹的canonical子集加入結果集。
Analysis
因爲搜索路徑有O(log n)個結點,故有O(log n)個canonical子集,所以可以在O(log n)時間內找到。
得到結果集合,可以在線性時間內遍歷這些子樹,時間複雜度爲O(k)。
如果只需要得到範圍內點的數量,可以在每個結點中記錄canonical集合的元素個數。
數據結構的空間複雜度爲O(n),它查詢一個範圍的時間複雜度爲O(log n)。
Multi-Dimensional Data
如何在高維空間中進行範圍搜索?
kD-tree [Jon Bertley 1975]。它是k-dimensional tree的縮寫。
它適用於一般的任意維空間。漸近搜索複雜度不是很好。
通過在x-y座標上分裂這種方式來推廣一維的樹。在k維空間中,將會循環所有維座標。
KD-Trees
一個二叉樹,每個結點有兩個值,所分裂的維,和分裂的值。
如果分裂在x座標的s處,那麼左子樹的點都有x座標<=s,右子樹的點都有x座標>s。對y座標也是一樣。
當只有O(1)個點剩餘時,將它們放到一個葉子結點。
只有葉子結點中有點位置的信息,非葉子結點只用於分裂。
Splitting
爲得到一個平衡樹,使用座標中位數來分裂——中位數本身放到左子樹或是右子樹都可以。
使用中位數分裂,可以保證樹的高度爲O(log n)。
分裂可以使用循環所有維的方式,或是根據數據做出選擇,比如,可以選擇數據分佈最廣的維。
Space Partitioning View
kD-tree是對空間的劃分,每個結點都會引入對x軸或是y軸的劃分。
點被劃分成兩部分,這兩部分分別是左子樹和右子樹。
這些劃分由矩形區域組成,稱爲單元(cell)(可能是沒有邊界的)。
根對應的是整個空間,隨後每個子結點都只繼承一半的空間。
葉子對應的是終止單元。
它是二分空間(Binary space Partitioning)的一種特殊情況。
construction
它可以以遞歸方式在O(n log n)時間內建成。
預先對x座標和y座標排序,並將兩個排序數組交叉鏈接起來。(譯註,即將每個點的x,y座標鏈接起來)。
比如,可以通過掃描x數組得到x座標的中位數,將數組分成兩部分。再用交叉鏈接將y數組在O(n)時間複雜度內分成兩部分。
現在有兩個子問題,每個都有n/2的大小,並且它們都有自己的排序數據。遞歸。
遞歸式爲T(n)=2T(n/2) + n,故它的時間複雜度爲T(n) = O(n log n)。(譯註,可以參考算法導論4.3節的主方法)。
Searching kD-Trees
設查詢矩形爲R。從根結點開始查找。
假設當前分裂的線是垂直的(也可以是水平的)。令v,w分別是左子結點和右子結點。
如果v是一個葉子結點,返回cell(v)交R,如果cell(v)屬於R,返回cell(v)中所有點,如果cell(v)交R爲空集,跳過。
對w進行相同的處理。
查詢過程明顯是沒有問題的。那麼它的時間複雜度是什麼呢?
Search complexity
當cell(v)屬於R,時間複雜度與cell(v)的大小成線性關係。
結點v訪問的結點個數滿足由cell(v)與R交集邊界的限定。
如果cell(v)在R區域之外,我們就不用去查找它,如果cell(v)在R之內,我們就要枚舉v中的所有點。只有在cell(v)與R局部重疊時纔會去遞歸調用。kD-Tree的高度是O(log n)。
令l爲穿過R的一條直線。
有多少個單元會與這條線相交?
因爲分裂的維是從兩個維中選擇一個,所以思考的關鍵在於每次將樹的兩層一起考慮。
假設第一次劃分是垂直的,第二次是水平的,我們就有四個單元,每一個有n/4個點。
一條直接只與兩個單元相交。單元或是在R區域之內或是R區域之外。
遞歸式爲:
從遞歸式可以得到時間複雜度爲:Q(n)=O(n1/2)。
kD-Tree是一個有O(n)空間複雜度的數據結構,它可以在最壞時間O(n1/2+m)內完成二維空間範圍查找,其中m是輸出的大小。
d-Dim Search Complexity
更高維的時間複雜度是什麼呢?
先嚐試三維情況,再推廣。
遞歸式是:
Q(n)=2d-1Q(n/2d)+1
從它推出時間複雜度:
Q(n)=O(n1-1/d)
kD-Tree是一個有(nd)空間複雜度的數據結構,它在d維空間中查找範圍的最壞時間複雜度爲O(n1-1/d+m),其中m是輸出的大小。
Orthogonal Range Trees
將一維查找樹推廣到d維空間。
每次查找都會遞歸地分解到多個低維空間中去查找。
查找時間複雜度爲O((log n)d+k),其中k是結果集大小。
時間和空間複雜度是O(n(log n)d-1)。
分散層疊(fractional cascading)從查找時間中消除了一個log n因子。
我們着眼於二維情況,但是它的思想是可以擴展到多維空間的。
2D Range Tree
令二維空間中的一個點集合爲P={p1, p2, ...,pn}。
一個查詢的一般形式爲:R=[xlo,xhi]*[ylo,yhi]。
我們開始不考慮y座標,在P上建立一個一維的x區域樹。
在[xlo,xhi]區間的點集屬於O(log n)個canonical集合。
這是一個結果的超集。它明顯比|R交P|要大,所以我們不能採用依次查看每個點在不在canonical集合中這種耗時的方式。
Level 2 Trees
思想的核心是取得每個canonical集合的點,並在它們上面建立y區域樹。
比如,canonical集合{9,12,14,15},用它的y座標建立了一個新的區域樹。
我們查找每個O(log n) canonical集合需要查找x區域樹中的[xlo,xhi]範圍和用它們的y區域樹查找[ylo,yhi]範圍。
y區域的查找中得到R交P這個結果集。
Canonical Sets
analysis
二維的查找時間複雜度爲O((log n)2)。
1. 查找O(log n)個canonical集合。
2. 每個集合進行y範圍查找需要O(log n)時間複雜性。
空間複雜度爲O(n log n)。
1. x區域樹中cononical集合的總大小是多少?
2. 非葉子結點個數=葉子結點個數。
3. 有一個大小爲n的集合,二個大小爲n/2的集合,以此類推。
4. 總共是O(n log n)。
5. 每個大小爲m的canonical集合需要O(m)的空間來存儲y區域樹。
6. 所以,總的空間複雜度是O(n log n)。
construction
x區域樹可以在O(n log n)時間內建立。
初看起來,因爲y樹的總大小是O(n log n),所以它需要O(n(log n)2)的時間來建立。
但是通過自底向上建立它們,我們可以避免在每個結點上的排序時間。
一旦子結點的y區域樹建立了,我們就可以將它們的y數組在線性時間內合併。
建立一維區域樹在排序後的時間複雜度是線性的。
所以,總的時間複雜度是O(n log n),是所有y區域樹的總大小。
d-Dim Range Trees
多級區域樹的思想可以很自然地擴展到任意d維。
在第一維上建立一個x區域樹。
在樹中每個結點v上,在v結點的cononical集合上爲剩餘的(d-1)維建立一個(d-1)維的區域樹。
查找時間複雜度每一維增加log n因子——每一維都會增加canonical集合總大小log n倍。
所以查找時間爲O((log n)d)。
建樹的空間和時間複雜度是O(n(log n)d-1)。
Fractional Cascading
有一種方法可以提高區域樹的查找時間log n。二維查找可以在O(log n)時間內完成。
基本思想:區域樹首先找到在[xlo,xhi]區間的點集,這些點集是由O(log n)個canonical集合組成的。
接下來,每個canonical集合用y區域樹查找[ylo,yhi]。我們先找到ylo的位置,然後一直讀到yhi邊界。
我們接下來要做的是,首先在O(log n)時間內進行第一次查找,但是會利用查找的信息到另一種數據結構上更高效地查找。
方法的關鍵是在canonical集合上加上一些指針。
Basic Idea
我們通過一個簡單的例子來了解這種方法的基本思想。
假設我們有兩個數組,A1和A2,它們都是無序的。
給定一個範圍[x,x’],我們想得到A1和A2中所有在這個範圍中的元素。
一個直觀的做法是用兩分查找找到A1和A2中k的位置,它的時間複雜度是O(2log n + k)。
Fractional Cascading Idea
假設有A2屬於A1,添加一些從A1指向A2的指針。
如果A1[i]=yi,將一個指針指向A2中大於yi的最小元素。
假設我們想得到範圍[y,y’]中的元素。
在A1中查找y,然後一直遍歷到y’,時間複雜度爲O(log n + k1)。
如果A1在A1[i]位置找到y,可以使用它的指針開始在A2中的查找。這一步的時候複雜度爲O(1 + k2)。
例子所示爲查找[20,65]。
FC in Range Trees
元素的特點:S(l(v))和S(r(v))的canonical集合是S(v)的canonical集合的子集。
X區域樹還是和以前一樣。但在這裏不建立y區域樹,而是根據y座標將它們保存到排序數組裏。
在A(v)中的每個entry保存兩個指針,指向A(l(v))和A(r(v))數組。
如果A(v)[i]保存着點p,那麼指針指向A(l(v))中最小大於的y座標元素(y(p))。對r(v)也是一樣。
Range Tree FC
下圖中給出了一部分指針的示例。
FC Search
考慮一個區域R=[x,x’]*[y,y’]。
在x區域樹中查找x, x’。
令vsplit爲兩條查找路徑開分開的結點。
O(log n)個canonical子集對應vsplit下的子結點,它們是在查找x(x’)路徑向左(右)時的右(左)子結點。
在vsplit結點,在A(vsplit)中二分查找y,時間複雜度爲O(log n)。
用我們x區域樹查找x,x’的方式向下查找。從entries的指向得到指向子結點中>=y的位置,這個操作的時間複雜度爲O(1)。
令A(v)是O(log n)個canonical結點的一個,現在要在A(v)中查找[y,y’]區域內的元素。
我們只需要找到A(v)>=y的最小entry。
我們可以在O(1)時間複雜度內找到是因爲parent(v)在查找路徑上,並且我們通過v數組中的指針,知道最小>=y的entry在A(parent(v))中的位置,
所以我們可以將在A(v)中在[y,y’]中的所有點在O(1 + kv)時間複雜度內輸出,其中kv是結果集大小。
對二維範圍搜索,最終的時間複雜度爲O(log n + k),空間複雜度爲O(n log n)。
d維範圍搜索使用分散層疊的時間複雜度爲O((log n)d-1)。