1 引言
每天我们晚上加班回家,可能都会用到滴滴或者共享单车。打开应用会看到如下的界面:
![bca0053213c85032807a1f1172631346.png](https://i-blog.csdnimg.cn/blog_migrate/c5b09cfec6180cf7e939f863d6fb434b.jpeg)
应用界面上会显示出附近某个范围内可用的出租车或者共享单车。假设地图上会显示以自己为圆心,5公里为半径,这个范围内的车。如何实现呢?最直观的想法就是去数据库里面查表,计算并查询车距离用户小于等于5公里的,筛选出来,把数据返回给客户端。
这种做法比较笨,一般也不会这么做。为什么呢?因为这种做法需要对整个表里面的每一项都计算一次相对距离。太耗时了。既然数据量太大,我们就需要分而治之。那么就会想到把地图分块。这样即使每一块里面的每条数据都计算一次相对距离,也比之前全表都计算一次要快很多。
本文来重点介绍一个比较通用的空间点索引算法:GeoHash算法。
2 GeoHash算法简介
Genhash 是一种地理编码,是由 Gustavo Niemeyer 发明的。它是一种分级的数据结构,其主要思想是把空间划分为网格。Genhash 属于空间填充曲线中的 Z 阶曲线(Z-order curve)的实际应用。
何为 Z 阶曲线?
![e6c7328a41f3c2c2bfb8d851beda2bea.png](https://i-blog.csdnimg.cn/blog_migrate/48ea35a28ed33d6fb32df483b13e3201.jpeg)
上图就是 Z 阶曲线。这个曲线比较简单,生成它也比较容易,只需要把每个 Z 首尾相连即可。
说到这里,读者可能依旧一头雾水,不知道 Geohash 和 Z 曲线究竟有啥关系?其实 Geohash算法的理论基础就是基于 Z 曲线的生成原理。
Geohash 能够提供任意精度的分段级别。一般分级从 1-12 级。
![f05a3faddbdd5b2228c720a2686898a2.png](https://i-blog.csdnimg.cn/blog_migrate/bd13216567a8f4db15dcd505e4aff2d1.jpeg)
还记得引言里面提到的问题么?这里我们就可以用 Geohash 来解决这个问题。
我们可以利用 Geohash 的字符串长短来决定要划分区域的大小。这个对应关系可以参考上面表格里面 cell 的宽和高。一旦选定 cell 的宽和高,那么 Geohash 字符串的长度就确定下来了。这样我们就把地图分成了一个个的矩形区域了。
地图上虽然把区域划分好了,但是还有一个问题没有解决,那就是如何快速的查找一个点附近邻近的点和区域呢?
Geohash 有一个和 Z 阶曲线相关的性质,那就是一个点附近的地方(但不绝对) hash 字符串总是有公共前缀,并且公共前缀的长度越长,这两个点距离越近。
由于这个特性,Geohash 就常常被用来作为唯一标识符。用在数据库里面可用 Geohash 来表示一个点。Geohash 这个公共前缀的特性就可以用来快速的进行邻近点的搜索。越接近的点通常和目标点的 Geohash 字符串公共前缀越长(但是这不一定,也有特殊情况,下面举例会说明)
Geohash 也有几种编码形式,常见的有2种,base 32 和 base 36。
![4262e91c62ce3425ad7622b766c2d1a4.png](https://i-blog.csdnimg.cn/blog_migrate/72b219e997abc06cb12e3f840e245c9c.jpeg)
base 36 的版本对大小写敏感,用了36个字符,“23456789bBCdDFgGhHjJKlLMnNPqQrRtTVWX”。
![7fae131664b18609102f1238090c1493.png](https://i-blog.csdnimg.cn/blog_migrate/75cd4602f6588fbf5a0f98a599558573.jpeg)
3 GeoHash实际应用
我们以 base-32 为例:
![985e11a72a15df334cd136bfd7ccb271.png](https://i-blog.csdnimg.cn/blog_migrate/64c2bc0b117a9dcdd7f79665890b4c22.jpeg)
上图是一个地图,地图中间有一个美罗城,假设需要查询距离美罗城最近的餐馆,该如何查询?
第一步我们需要把地图网格化,利用 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。一直划分下去。
![792c3d871d6c0cf84cf36c077bdef6a8.png](https://i-blog.csdnimg.cn/blog_migrate/2d471914c02d1ad06959fa3aa8fc2171.jpeg)
再处理经度,一样的处理方式。地球经度区间是[-180,180]
![5e7c28a1b26f3c20e253c25474601ae1.png](https://i-blog.csdnimg.cn/blog_migrate/33f009a6942d1f12542b110766f06e76.jpeg)
纬度产生的二进制是101011000101110,经度产生的二进制是110101100101101,按照“偶数位放经度,奇数位放纬度”的规则,重新组合经度和纬度的二进制串,生成新的:111001100111100000110011110110,最后一步就是把这个最终的字符串转换成字符,对应需要查找 base-32 的表。11100 11001 11100 00011 00111 10110转换成十进制是 28 25 28 3 7 22,查表编码得到最终结果,wtw37q。
我们还可以把这个网格周围8个各自都计算出来。
![ee3b88aed22f5293e814abf29d968dd9.png](https://i-blog.csdnimg.cn/blog_migrate/01078f7faaa31d93786e05b74d341cc3.jpeg)
从地图上可以看出,这邻近的9个格子,前缀都完全一致。都是wtw37。
如果我们把字符串再增加一位,会有什么样的结果呢?Geohash 增加到7位。
![cfd52a813ac574e3c629e752d38e2b5b.png](https://i-blog.csdnimg.cn/blog_migrate/c498355dc6e83657eea29aa8594de987.jpeg)
当Geohash 增加到7位的时候,网格更小了,美罗城的 Geohash 变成了 wtw37qt。
看到这里,读者应该已经清楚了Geohash 算法的原理了。我们把6位和7位都组合到一张图上面来看。
![a063ca46395e6364f23f9cc9cd8ce3e2.png](https://i-blog.csdnimg.cn/blog_migrate/5cf5b0ba3165c1178c17beea61f2c995.jpeg)
可以看到中间大格子的 Geohash 的值是 wtw37q,那么它里面的所有小格子前缀都是 wtw37q。可以想象,当 Geohash 字符串长度为5的时候,Geohash 肯定就为 wtw37 了。
接下来解释之前说的 Geohash 和 Z 阶曲线的关系。回顾最后一步合并经纬度字符串的规则,“偶数位放经度,奇数位放纬度”。读者一定有点好奇,这个规则哪里来的?凭空瞎想的?其实并不是,这个规则就是 Z 阶曲线。看下图:
![0d405bd0dcca231ff00aa6671835cad0.png](https://i-blog.csdnimg.cn/blog_migrate/80dfedc7f0c72d9a15b2b69d0d668380.jpeg)
x 轴就是纬度,y轴就是经度。经度放偶数位,纬度放奇数位就是这样而来的。
最后有一个精度的问题,下面的表格数据一部分来自 Wikipedia。
Geohash 字符串长度 纬度 经度 纬度误差 经度误差 km误差。
![c99f8ac332e0514d02406f73f85801aa.png](https://i-blog.csdnimg.cn/blog_migrate/421efbb82cafc2d83fb5c5eefe5a759a.jpeg)
4 小结
那么今天对于GeoHash算法的科普就结束了,如果有任何问题,欢迎评论。笔者后续会持续分享地图检索相关的文章。
原文链接:https://www.cnblogs.com/mafeng/p/7910129.html
欢迎关注笔者,每天分享架构干货。