空间点像素索引(一)
空間點像素索引(一)
給你一個需求,查找定位點附近的一定范圍內所有餐館,你會怎么實現呢?
一一計算定位點與所有餐館的距離,然后取出最小的距離?如果同時有很多遍布全國的請求都在查找附近的餐館,按照上述的做法,你的服務有能力及時響應么?本文介紹了兩種高效的空間點查找方法,并且該方法在打車領域和地圖領域有廣泛的實際應用。文章很長,如果來不及看完,只需要記得,如果你需要一種高效的空間點索引算法來處理海量的空間點查找需求,那么Geohash和Google S2可以幫助到你。
引論
每天我們晚上加班回家,可能都會用到滴滴或者共享單車。打開 app 會看到如下的界面:
app 界面上會顯示出自己附近一個范圍內可用的出租車或者共享單車。假設地圖上會顯示以自己為圓心,5公里為半徑,這個范圍內的車。如何實現呢?最直觀的想法就是去數據庫里面查表,計算并查詢車距離用戶小于等于5公里的,篩選出來,把數據返回給客戶端。這種做法比較笨,一般也不會這么做。為什么呢?因為這種做法需要對整個表里面的每一項都計算一次相對距離。太耗時了。既然數據量太大,我們就需要分而治之。那么就會想到把地圖分塊。這樣即使每一塊里面的每條數據都計算一次相對距離,也比之前全表都計算一次要快很多。我們也都知道,現在用的比較多的數據庫 MySQL、PostgreSQL 都原生支持 B+ 樹。這種數據結構能高效的查詢。地圖分塊的過程其實就是一種添加索引的過程,如果能想到一個辦法,把地圖上的點添加一個合適的索引,并且能夠排序,那么就可以利用類似二分查找的方法進行快速查詢。問題就來了,地圖上的點是二維的,有經度和緯度,這如何索引呢?如果只針對其中的一個維度,經度或者緯度進行搜索,那搜出來一遍以后還要進行二次搜索。那要是更高維度呢?三維。可能有人會說可以設置維度的優先級,比如拼接一個聯合鍵,那在三維空間中,x,y,z 誰的優先級高呢?設置優先級好像并不是很合理。本篇文章就來介紹2種比較通用的空間點索引算法。
一. GeoHash 算法
- Geohash 算法簡介
Geohash 是一種地理編碼,由 Gustavo Niemeyer 發明的。它是一種分級的數據結構,把空間劃分為網格。Geohash 屬于空間填充曲線中的 Z 階曲線(Z-order curve)的實際應用。何為 Z 階曲線?
上圖就是 Z 階曲線。這個曲線比較簡單,生成它也比較容易,只需要把每個 Z 首尾相連即可。
Z 階曲線同樣可以擴展到三維空間。只要 Z 形狀足夠小并且足夠密,也能填滿整個三維空間。說到這里可能讀者依舊一頭霧水,不知道 Geohash 和 Z 曲線究竟有啥關系?其實 Geohash算法 的理論基礎就是基于 Z 曲線的生成原理。繼續說回 Geohash。Geohash 能夠提供任意精度的分段級別。一般分級從 1-12 級。
還記得引語里面提到的問題么?這里我們就可以用 Geohash 來解決這個問題。我們可以利用 Geohash 的字符串長短來決定要劃分區域的大小。這個對應關系可以參考上面表格里面 cell 的寬和高。一旦選定 cell 的寬和高,那么 Geohash 字符串的長度就確定下來了。這樣我們就把地圖分成了一個個的矩形區域了。地圖上雖然把區域劃分好了,但是還有一個問題沒有解決,那就是如何快速的查找一個點附近鄰近的點和區域呢?Geohash 有一個和 Z 階曲線相關的性質,那就是一個點附近的地方(但不絕對) hash 字符串總是有公共前綴,并且公共前綴的長度越長,這兩個點距離越近。由于這個特性,Geohash 就常常被用來作為唯一標識符。用在數據庫里面可用 Geohash 來表示一個點。Geohash 這個公共前綴的特性就可以用來快速的進行鄰近點的搜索。越接近的點通常和目標點的 Geohash 字符串公共前綴越長(但是這不一定,也有特殊情況,下面舉例會說明)Geohash 也有幾種編碼形式,常見的有2種,base 32 和 base 36。
base 36 的版本對大小寫敏感,用了36個字符,“23456789bBCdDFgGhHjJKlLMnNPqQrRtTVWX”。
- geohash實際應用舉例
接下來的舉例以 base-32 為例。舉個例子。
上圖是一個地圖,地圖中間有一個美羅城,假設需要查詢距離美羅城最近的餐館,該如何查詢?第一步我們需要把地圖網格化,利用 geohash。通過查表,我們選取字符串長度為6的矩形來網格化這張地圖。經過查詢,美羅城的經緯度是[31.1932993, 121.43960190000007]。先處理緯度。地球的緯度區間是[-90,90]。把這個區間分為2部分,即[-90,0),[0,90]。31.1932993位于(0,90]區間,即右區間,標記為1。然后繼續把(0,90]區間二分,分為[0,45),[45,90],31.1932993位于[0,45)區間,即左區間,標記為0。一直劃分下去。
再處理經度,一樣的處理方式。地球經度區間是[-180,180]
緯度產生的二進制是101011000101110,經度產生的二進制是110101100101101,按照**“偶數位放經度,奇數位放緯度”**的規則,重新組合經度和緯度的二進制串,生成新的:111001100111100000110011110110,最后一步就是把這個最終的字符串轉換成字符,對應需要查找 base-32 的表。11100 11001 11100 00011 00111 10110轉換成十進制是 28 25 28 3 7 22,查表編碼得到最終結果,wtw37q。我們還可以把這個網格周圍8個各自都計算出來。
從地圖上可以看出,這鄰近的9個格子,前綴都完全一致。都是wtw37。如果我們把字符串再增加一位,會有什么樣的結果呢?Geohash 增加到7位。
當Geohash 增加到7位的時候,網格更小了,美羅城的 Geohash 變成了 wtw37qt。看到這里,讀者應該已經清楚了 Geohash 的算法原理了。咱們把6位和7位都組合到一張圖上面來看。
可以看到中間大格子的 Geohash 的值是 wtw37q,那么它里面的所有小格子前綴都是 wtw37q。可以想象,當 Geohash 字符串長度為5的時候,Geohash 肯定就為 wtw37 了。接下來解釋之前說的 Geohash 和 Z 階曲線的關系。回顧最后一步合并經緯度字符串的規則,“偶數位放經度,奇數位放緯度”。讀者一定有點好奇,這個規則哪里來的?憑空瞎想的?其實并不是,這個規則就是 Z 階曲線。看下圖:
x 軸就是緯度,y軸就是經度。經度放偶數位,緯度放奇數位就是這樣而來的。最后有一個精度的問題,下面的表格數據一部分來自 Wikipedia。
- Geohash具體實現
到此,讀者應該對 Geohash 的算法都很明了了。接下來用 Go 實現一下 Geohash 算法。
package geohash
import (
“bytes”
)
const (
BASE32 = “0123456789bcdefghjkmnpqrstuvwxyz”
MAX_LATITUDE float64 = 90
MIN_LATITUDE float64 = -90
MAX_LONGITUDE float64 = 180
MIN_LONGITUDE float64 = -180
)
var (
bits = []int{16, 8, 4, 2, 1}
base32 = []byte(BASE32)
)
type Box struct {
MinLat, MaxLat float64 // 緯度
MinLng, MaxLng float64 // 經度
}
func (this
*Box) Width() float64 {
return this.MaxLng
- this.MinLng
}
func (this
*Box) Height() float64 {
return
this.MaxLat - this.MinLat
}
// 輸入值:緯度,經度,精度(geohash的長度)
// 返回geohash, 以及該點所在的區域
func Encode(latitude, longitude float64, precision int) (string, *Box) {
var geohash
bytes.Buffer
var minLat,
maxLat float64 =
MIN_LATITUDE, MAX_LATITUDE
var minLng,
maxLng float64 =
MIN_LONGITUDE, MAX_LONGITUDE
var mid float64 = 0
bit, ch, length, isEven := 0, 0, 0, true
for length
< precision {
if isEven {
if mid =
(minLng + maxLng) / 2; mid < longitude {
ch |= bits[bit]
minLng = mid
} else {
maxLng = mid
}
} else {
if mid =
(minLat + maxLat) / 2; mid < latitude {
ch |= bits[bit]
minLat = mid
} else {
maxLat = mid
}
}
isEven = !isEven
if bit <
4 {
bit++
} else {
geohash.WriteByte(base32[ch])
length, bit, ch = length+1, 0, 0
}
}
b := &Box{
MinLat: minLat,
MaxLat: maxLat,
MinLng: minLng,
MaxLng: maxLng,
}
return
geohash.String(), b
}
- Geohash優缺點
Geohash 的優點很明顯,它利用 Z 階曲線進行編碼。而 Z 階曲線可以將二維或者多維空間里的所有點都轉換成一維曲線。在數學上成為分形維。并且 Z 階曲線還具有局部保序性。Z 階曲線通過交織點的坐標值的二進制表示來簡單地計算多維度中的點的z值。一旦將數據被加到該排序中,任何一維數據結構,例如二叉搜索樹,B樹,跳躍表或(具有低有效位被截斷)哈希表 都可以用來處理數據。通過 Z 階曲線所得到的順序可以等同地被描述為從四叉樹的深度優先遍歷得到的順序。這也是 Geohash 的另外一個優點,搜索查找鄰近點比較快。Geohash 的缺點之一也來自 Z 階曲線。Z 階曲線有一個比較嚴重的問題,雖然有局部保序性,但是它也有突變性。在每個 Z 字母的拐角,都有可能出現順序的突變。
看上圖中標注出來的藍色的點點。每兩個點雖然是相鄰的,但是距離相隔很遠。看右下角的圖,兩個數值鄰近紅色的點兩者距離幾乎達到了整個正方形的邊長。兩個數值鄰近綠色的點也達到了正方形的一半的長度。Geohash 的另外一個缺點是,如果選擇不好合適的網格大小,判斷鄰近點可能會比較麻煩。
看上圖,如果選擇 Geohash 字符串為6的話,就是藍色的大格子。紅星是美羅城,紫色的圓點是搜索出來的目標點。如果用 Geohash 算法查詢的話,距離比較近的可能是 wtw37p,wtw37r,wtw37w,wtw37m。但是其實距離最近的點就在 wtw37q。如果選擇這么大的網格,就需要再查找周圍的8個格子。如果選擇 Geohash 字符串為7的話,那變成黃色的小格子。這樣距離紅星星最近的點就只有一個了。就是 wtw37qw。如果網格大小,精度選擇的不好,那么查詢最近點還需要再次查詢周圍8個點。
二. 空間填充曲線和 分形
在介紹第二種多維空間點索引算法之前,要先談談空間填充曲線(Space-filling curve)和分形。解決多維空間點索引需要解決2個問題,第一,如何把多維降為低維或者一維?第二,一維的曲線如何分形?
- 空間曲線
在數學分析中,有這樣一個難題:能否用一條無限長的線,穿過任意維度空間里面的所有點?
在1890年,Giuseppe Peano 發現了一條連續曲線,現在稱為 Peano 曲線,它可以穿過單位正方形上的每個點。他的目的是構建一個可以從單位區間到單位正方形的連續映射。Peano 受到 Georg Cantor 早期違反直覺的研究結果的啟發,即單位區間中無限數量的點與任何有限維度流型(manifold)中無限數量的點,基數相同。Peano 解決的問題實質就是,是否存在這樣一個連續的映射,一條能填充滿平面的曲線。上圖就是他找到的一條曲線。一般來說,一維的東西是不可能填滿2維的方格的。但是皮亞諾曲線恰恰給出了反例。皮亞諾曲線是一條連續的但處處不可導的曲線。皮亞諾曲線的構造方法如下:取一個正方形并且把它分出九個相等的小正方形,然后從左下角的正方形開始至右上角的正方形結束,依次把小正方形的中心用線段連接起來;下一步把每個小正方形分成九個相等的正方形,然后上述方式把其中中心連接起來……將這種操作手續無限進行下去,最終得到的極限情況的曲線就被稱作皮亞諾曲線。皮亞諾對區間[0,1]上的點和正方形上的點的映射作了詳細的數學描述。實際上,正方形的這些點對于,可找到兩個連續函數 x = f(t) 和 y = g(t),使得 x 和 y 取屬于單位正方形的每一個值。一年后,即1891年,希爾伯特就作出了這條曲線,叫希爾伯特曲線(Hilbert curve)。
上圖就是1-6階的希爾伯特曲線。具體構造方式在下一章再說。
上圖是希爾伯特曲線填充滿3維空間。之后還有很多變種的空間填充曲線,龍曲線(Dragon curve)、
高斯帕曲線(Gosper curve)、Koch曲線(Koch curve)、摩爾定律曲線(Moore curve)、謝爾賓斯基曲線(Sierpiński curve)、奧斯古德曲線(Osgood curve)。這些曲線和本文無關,就不詳細介紹了。
在數學分析中,空間填充曲線是一個參數化的注入函數,它將單位區間映射到單位正方形,立方體,更廣義的,n維超立方體等中的連續曲線,隨著參數的增加,它可以任意接近單位立方體中的給定點。除了數學重要性之外,空間填充曲線也可用于降維,數學規劃,稀疏多維數據庫索引,電子學和生物學。空間填充曲線的現在被用在互聯網地圖中。
- 分形
皮亞諾曲線的出現,說明了人們對維數的認識是有缺陷的,有必要重新考察維數的定義。這就是分形幾何考慮的問題。在分形幾何中,維數可以是分數叫做分維。多維空間降維以后,如何分形,也是一個問題。分形的方式有很多種,這里有一個列表,可以查看如何分形,以及每個分形的分形維數,即豪斯多夫分形維(Hausdorff fractals dimension)和拓撲維數。這里就不細說分形的問題了,感興趣的可以仔細閱讀鏈接里面的內容。接下來繼續來說多維空間點索引算法,下面一個算法的理論基礎來自希爾伯特曲線,先來仔細說說希爾伯特曲線。
總結
以上是生活随笔為你收集整理的空间点像素索引(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 相机标定实用方案
- 下一篇: 空间点像素索引(二)