geohash演算法原理及實現方式 1、geohash特點
4、觀點討論
geohash有以下幾個特點:
首先,geohash用一個字串表示經度和緯度兩個座標。某些情況下無法在兩列上同時應用索引 (例如MySQL 4之前的版本,Google App Engine的資料層等),利用geohash,只需在一列上應用索引即可。
其次,geohash表示的並不是一個點,而是一個矩形區域。比如編碼wx4g0ec19,它表示的是一個矩形區域。 使用者可以釋出地址編碼,既能表明自己位於北海公園附近,又不至於暴露自己的精確座標,有助於隱私保護。
第三,編碼的字首可以表示更大的區域。例如wx4g0ec1,它的字首wx4g0e表示包含編碼wx4g0ec1在內的更大範圍。 這個特性可以用於附近地點搜尋。首先根據使用者當前座標計算geohash(例如wx4g0ec1)然後取其字首進行查詢 (SELECT * FROM place WHERE geohash LIKE 'wx4g0e%'),即可查詢附近的所有地點。
Geohash比直接用經緯度的高效很多。
Geohash的原理
Geohash的最簡單的解釋就是:將一個經緯度資訊,轉換成一個可以排序,可以比較的字串編碼
首先將緯度範圍(-90, 90)平分成兩個區間(-90,0)、(0, 90),如果目標緯度位於前一個區間,則編碼為0,否則編碼為1。
由於39.92324屬於(0, 90),所以取編碼為1。
然後再將(0, 90)分成 (0, 45), (45, 90)兩個區間,而39.92324位於(0, 45),所以編碼為0。
以此類推,直到精度符合要求為止,得到緯度編碼為1011 1000 1100 0111 1001。
緯度範圍 |
劃分區間0 |
劃分區間1 |
39.92324所屬區間 |
(-90, 90) |
(-90, 0.0) |
(0.0, 90) |
1 |
(0.0, 90) |
(0.0, 45.0) |
(45.0, 90) |
0 |
(0.0, 45.0) |
(0.0, 22.5) |
(22.5, 45.0) |
1 |
(22.5, 45.0) |
(22.5, 33.75) |
(33.75, 45.0) |
1 |
(33.75, 45.0) |
(33.75, 39.375) |
(39.375, 45.0) |
1 |
(39.375, 45.0) |
(39.375, 42.1875) |
(42.1875, 45.0) |
0 |
(39.375, 42.1875) |
(39.375, 40.7812) |
(40.7812, 42.1875) |
0 |
(39.375, 40.7812) |
(39.375, 40.0781) |
(40.0781, 40.7812) |
0 |
(39.375, 40.0781) |
(39.375, 39.7265) |
(39.7265, 40.0781) |
1 |
(39.7265, 40.0781) |
(39.7265, 39.9023) |
(39.9023, 40.0781) |
1 |
(39.9023, 40.0781) |
(39.9023, 39.9902) |
(39.9902, 40.0781) |
0 |
(39.9023, 39.9902) |
(39.9023, 39.9462) |
(39.9462, 39.9902) |
0 |
(39.9023, 39.9462) |
(39.9023, 39.9243) |
(39.9243, 39.9462) |
0 |
(39.9023, 39.9243) |
(39.9023, 39.9133) |
(39.9133, 39.9243) |
1 |
(39.9133, 39.9243) |
(39.9133, 39.9188) |
(39.9188, 39.9243) |
1 |
(39.9188, 39.9243) |
(39.9188, 39.9215) |
(39.9215, 39.9243) |
1 |
經度也用同樣的演算法,對(-180, 180)依次細分,得到116.3906的編碼為1101 0010 1100 0100 0100。
經度範圍 |
劃分區間0 |
劃分區間1 |
116.3906所屬區間 |
(-180, 180) |
(-180, 0.0) |
(0.0, 180) |
1 |
(0.0, 180) |
(0.0, 90.0) |
(90.0, 180) |
1 |
(90.0, 180) |
(90.0, 135.0) |
(135.0, 180) |
0 |
(90.0, 135.0) |
(90.0, 112.5) |
(112.5, 135.0) |
1 |
(112.5, 135.0) |
(112.5, 123.75) |
(123.75, 135.0) |
0 |
(112.5, 123.75) |
(112.5, 118.125) |
(118.125, 123.75) |
0 |
(112.5, 118.125) |
(112.5, 115.312) |
(115.312, 118.125) |
1 |
(115.312, 118.125) |
(115.312, 116.718) |
(116.718, 118.125) |
0 |
(115.312, 116.718) |
(115.312, 116.015) |
(116.015, 116.718) |
1 |
(116.015, 116.718) |
(116.015, 116.367) |
(116.367, 116.718) |
1 |
(116.367, 116.718) |
(116.367, 116.542) |
(116.542, 116.718) |
0 |
(116.367, 116.542) |
(116.367, 116.455) |
(116.455, 116.542) |
0 |
(116.367, 116.455) |
(116.367, 116.411) |
(116.411, 116.455) |
0 |
(116.367, 116.411) |
(116.367, 116.389) |
(116.389, 116.411) |
1 |
(116.389, 116.411) |
(116.389, 116.400) |
(116.400, 116.411) |
0 |
(116.389, 116.400) |
(116.389, 116.394) |
(116.394, 116.400) |
0 |
接下來將經度和緯度的編碼合併,奇數位是緯度,偶數位是經度,得到編碼 11100 11101 00100 01111 00000 01101 01011 00001。
最後,用0-9、b-z(去掉a, i, l, o)這32個字母進行base32編碼,得到(39.92324, 116.3906)的編碼為wx4g0ec1。
十進位制 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
base32 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
b |
c |
d |
e |
f |
g |
十進位制 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
base32 |
h |
j |
k |
m |
n |
p |
q |
r |
s |
t |
u |
v |
w |
x |
y |
z |
解碼演算法與編碼演算法相反,先進行base32解碼,然後分離出經緯度,最後根據二進位制編碼對經緯度範圍進行細分即可,這裡不再贅述。
實現程式碼:
php:
geohash.class.php
View Codepython:
java:
View CodeC#:
C#版本的geohash代1 using System; 2 3 namespace sharonjl.utils 4 { 5 public static class Geohash 6 { 7 #region Direction enum 8 9 public enum Direction 10 { 11 Top = 0, 12 Right = 1, 13 Bottom = 2, 14 Left = 3 15 } 16 17 #endregion 18 19 private const string Base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; 20 private static readonly int[] Bits = new[] {16, 8, 4, 2, 1}; 21 22 private static readonly string[][] Neighbors = { 23 new[] 24 { 25 "p0r21436x8zb9dcf5h7kjnmqesgutwvy", // Top 26 "bc01fg45238967deuvhjyznpkmstqrwx", // Right 27 "14365h7k9dcfesgujnmqp0r2twvyx8zb", // Bottom 28 "238967debc01fg45kmstqrwxuvhjyznp", // Left 29 }, new[] 30 { 31 "bc01fg45238967deuvhjyznpkmstqrwx", // Top 32 "p0r21436x8zb9dcf5h7kjnmqesgutwvy", // Right 33 "238967debc01fg45kmstqrwxuvhjyznp", // Bottom 34 "14365h7k9dcfesgujnmqp0r2twvyx8zb", // Left 35 } 36 }; 37 38 private static readonly string[][] Borders = { 39 new[] {"prxz", "bcfguvyz", "028b", "0145hjnp"}, 40 new[] {"bcfguvyz", "prxz", "0145hjnp", "028b"} 41 }; 42 43 public static String CalculateAdjacent(String hash, Direction direction) 44 { 45 hash = hash.ToLower(); 46 47 char lastChr = hash[hash.Length - 1]; 48 int type = hash.Length%2; 49 var dir = (int) direction; 50 string nHash = hash.Substring(0, hash.Length - 1); 51 52 if (Borders[type][dir].IndexOf(lastChr) != -1) 53 { 54 nHash = CalculateAdjacent(nHash, (Direction) dir); 55 } 56 return nHash + Base32[Neighbors[type][dir].IndexOf(lastChr)]; 57 } 58 59 public static void RefineInterval(ref double[] interval, int cd, int mask) 60 { 61 if ((cd & mask) != 0) 62 { 63 interval[0] = (interval[0] + interval[1])/2; 64 } 65 else 66 { 67 interval[1] = (interval[0] + interval[1])/2; 68 } 69 } 70 71 public static double[] Decode(String geohash) 72 { 73 bool even = true; 74 double[] lat = {-90.0, 90.0}; 75 double[] lon = {-180.0, 180.0}; 76 77 foreach (char c in geohash) 78 { 79 int cd = Base32.IndexOf(c); 80 for (int j = 0; j < 5; j++) 81 { 82 int mask = Bits[j]; 83 if (even) 84 { 85 RefineInterval(ref lon, cd, mask); 86 } 87 else 88 { 89 RefineInterval(ref lat, cd, mask); 90 } 91 even = !even; 92 } 93 } 94 95 return new[] {(lat[0] + lat[1])/2, (lon[0] + lon[1])/2}; 96 } 97 98 public static String Encode(double latitude, double longitude, int precision = 12) 99 { 100 bool even = true; 101 int bit = 0; 102 int ch = 0; 103 string geohash = ""; 104 105 double[] lat = {-90.0, 90.0}; 106 double[] lon = {-180.0, 180.0}; 107 108 if (precision < 1 || precision > 20) precision = 12; 109 110 while (geohash.Length < precision) 111 { 112 double mid; 113 114 if (even) 115 { 116 mid = (lon[0] + lon[1])/2; 117 if (longitude > mid) 118 { 119 ch |= Bits[bit]; 120 lon[0] = mid; 121 } 122 else 123 lon[1] = mid; 124 } 125 else 126 { 127 mid = (lat[0] + lat[1])/2; 128 if (latitude > mid) 129 { 130 ch |= Bits[bit]; 131 lat[0] = mid; 132 } 133 else 134 lat[1] = mid; 135 } 136 137 even = !even; 138 if (bit < 4) 139 bit++; 140 else 141 { 142 geohash += Base32[ch]; 143 bit = 0; 144 ch = 0; 145 } 146 } 147 return geohash; 148 } 149 } 150 }
C#程式碼來自:https://github.com/sharonjl/geohash-net
geohash演示:http://openlocation.org/geohash/geohash-js/
各種版本下載:打包下載
觀點討論
引用阿里雲以為技術專家的部落格上的討論:
1.兩個離的越近,geohash的結果相同的位數越多,對麼?這一點是有些使用者對geohash的誤解,雖然geo確實儘可能的將位置相近的點hash到了一起,可是這並不是嚴格意義上的(實際上也並不可能,因為畢竟多一維座標),
例如在方格4的左下部分的點和大方格1的右下部分的點離的很近,可是它們的geohash值一定是相差的相當遠,因為頭一次的分塊就相差太大了,很多時候我們對geohash的值進行簡單的排序比較,結果貌似真的能夠找出相近的點,並且似乎還是按照距離的遠近排列的,可是實際上會有一些點被漏掉了。
上述這個問題,可以通過搜尋一個格子,周圍八個格子的資料,統一獲取後再進行過濾。這樣就在編碼層次解決了這個問題。
2.既然不能做到將相近的點hash值也相近,那麼geohash的意義何在呢?
我覺得geohash還是相當有用的一個演算法,畢竟這個演算法通過無窮的細分,能確保將每一個小塊的geohash值確保在一定的範圍之內,這樣就為靈活的周邊查詢和範圍查詢提供了可能。
常見的一些應用場景
A、如果想查詢附近的點?如何操作
查出改點的gehash值,然後到資料庫裡面進行字首匹配就可以了。
B、如果想查詢附近點,特定範圍內,例如一個點周圍500米的點,如何搞?
可以查詢結果,在結果中進行賽選,將geohash進行解碼為經緯度,然後進行比較
*在緯度相等的情況下:
*經度每隔0.00001度,距離相差約1米;
*每隔0.0001度,距離相差約10米;
*每隔0.001度,距離相差約100米;
*每隔0.01度,距離相差約1000米;
*每隔0.1度,距離相差約10000米。
*在經度相等的情況下:
*緯度每隔0.00001度,距離相差約1.1米;
*每隔0.0001度,距離相差約11米;
*每隔0.001度,距離相差約111米;
*每隔0.01度,距離相差約1113米;
*每隔0.1度,距離相差約11132米。
Geohash,如果geohash的位數是6位數的時候,大概為附近1千米…
參考資料: