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