初探Uber H3原理

2018年初,Uber正式开源了他们自己的一种空间索引算法H3(H3项目地址:h3)。最近偶然得知了这个算法,于是Google了一下,发现Uber还是比较良心地在其官网上给出了很多算法原理的讲解: 简略版:H3: Uber's Hexagonal Hierarchical Spatial Index

详细版:H3 introdutction

如果英文还不错的话,建议直接看官方的介绍,我这里只是大致翻译和解释了一点原理层面的东西。

六边形网格索引

H3是一种基于网格的空间索引,但跟普通的矩形网格索引不同的是,他的每一个网格都是正六边形。为啥要选正六边形呢,因为在基于网格的空间索引中,使用的多边形的边数越多,则一个网格越近似圆形,做缓冲区查询、kNN查询什么的也就越方便。而做网格索引又要求空间能够被网格铺满,不能有缝隙。根据初中数学知识,我们知道一个多边形的内角和公式为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> θ = ( x − 2 ) ∗ 18 0 ∘ \theta = (x - 2) * 180^\circ </math>θ=(x−2)∗180∘

其中,x为多边形的边数, <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ为多边形的内角和。则一个正多边形的每个角的角度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ x = ( x − 2 ) ∗ 18 0 ∘ x \frac{\theta}{x}=\frac{(x - 2) * 180^\circ}{x} </math>xθ=x(x−2)∗180∘,而如果需要多边形能够铺满空间,则在多边形的顶点相交处,设每个顶点有y个多边形相交,需要满足以下等式:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 36 0 ∘ y = ( x − 2 ) ∗ 18 0 ∘ x \frac{360^\circ}{y} = \frac{(x - 2) * 180^\circ}{x} </math>y360∘=x(x−2)∗180∘

以上等式的求解过程我不再赘述,这个等式只有三组整数解:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> { x = 3 y = 6 { x = 4 y = 4 { x = 6 y = 3 \begin{cases} x=3&\\ y=6&\\ \end{cases} \begin{cases} x=4&\\ y=4&\\ \end{cases} \begin{cases} x=6&\\ y=3&\\ \end{cases} </math>{x=3y=6{x=4y=4{x=6y=3

因此,能够做网格空间索引的形状只有三角形、矩形和六边形,而六边形因为边数最多,最接近圆,所以理论上来说在某些场景下是最优的选择。

Uber的技术博客上也给出了使用六边形做空间索引的优越性:

上图展示了三种网格到其相邻网格的距离,可以看到,只有六边形到其相邻六边形的距离都是相等的。所以,H3就使用六边形作为网格索引的基本单元,实现空间索引。

无变形的投影

在GIS领域中,对空间填充曲线熟悉的同学应该知道,不论是GeoHash, Z2或者Hilbert,虽然看起来都是将空间按照经纬度分割成了一个个大小相等的网格,但实际上这些网格的实际面积并不相等。对于靠近极地的网格,虽然经纬度的间隔没变,但由于地球的曲率,这些网格的实际面积远小于靠近赤道的网格。 这种实际面积不相等的网格索引可能会造成一个问题,那就是由于网格大小不一致导致网格内数据量不一致,造成热网格和冷网格,Uber认为这会大大降低空间查询效率(笔者按:其实时空数据本身分布就十分不均匀,网格对应的实际大小不一致倒不是一个很大的影响,个人感觉在这一点上Uber有点言过其实 )。于是,H3干脆摒弃传统的地图投影,直接在地球上铺满六边形(如图2)。

图2 六边形铺满地球

但是,理想很美好,现实却需要考虑如何实现。这么多六边形,到高层级的时候很难对齐进行一一编码,这么看来使用六边形铺满地球不太现实。那怎么办呢?一个办法就是对这些六边形进行分区管理。H3实现的方法是:将地球当作一个二十面体,这个二十面体的每一个面都是球面三角形,有12个顶点,称为球形二十面体(spherical icosahedron),在这个球形十二面体的每个面上都有相同排列方式的六边形。由于这个球形二十面体的12个顶点每一个都在地球上的水里,可以保证对于每个面做处理时不会遇到边界的edge case,因为Uber还没有轮船快艇业务,只需要保证在陆地上H3好使就行。这个球形二十面体的示意图见图3。

图3 球形二十面体
再考虑到图2所示的六边形网格,在第0层,这个球形二十面体的每一个面长这样:

图4 二十面体的一个面
这样一来,可以看到,在一个球面三角形的顶角处有个小三角形,这个小三角形就是H3的一种edge case:在二十面体的顶点处,有五个面交于这个顶点,每个面在这个顶点处都有一个小三角形,所以这些小三角形会形成一个五边形。也就是说,H3并不能保证每个空间单元都是六边形,在一些地方还是会存在五边形,但是这样做也不会造成很大影响,因为根据球形二十面体每个顶点都在水里的特性,这种五边形只会出现在水域周围,不会对Uber的打车和外卖业务造成很大的影响。根据这样的索引特性,H3规定在索引的第0层,每个面和图4一样,每个面上有5.5个六边形和\\frac{3}{5}个五边形,即第0层一共有110个六边形和12个五边形。H3将这110个六边形称为基网格(base cells)。H3最高可以到15层,也就是说H3有16个层级的空间索引粒度,在粒度最细的第15层中,平均每个网格的大小为0.9平方米,平均边长为0.509713米。在往下一层划分时,每个父网格对应7个子网格,父子网格之间的对应关系是这样的:

爷爷网格 父亲网格 儿子网格

可以看到,父子网格之间并没有严密的对齐,父网格和其所对应的7个子网格之间会有一点差异,这种差异也导致了H3并不能表现出很好的层级关系。

编码方式

对六边形索引,H3使用了一种IJK坐标系来确定六边形的位置。IJK坐标系长这样:

图5 IJK坐标系
当然,H3不会简单地使用这种坐标系来对所有六边形进行编码,因为在每一个层级六边形的排列方式都不太一样。总的来说,在所有层级中,六边形的排列方式只有两种类型,称为Class II和Class III。在这两种类型的排列方式中,IJK坐标系的三个轴的方向不太一样。对于这种在一个二十面体的面上,根据不同的六边形排列方式使用不同方向坐标轴的坐标系,H3称之为FaceIJK坐标系:

图6 FaceIJK坐标系
那么对于一个面上所有层级的所有六边形,都使用FaceIJK坐标系编码,再加上面的唯一标识符可不可以?答案是可以,但没必要,因为如果这样做,H3编码会更加表示不了层级之间的关系。H3为了突出层级之间的关联性,使用了一种方法:每个六边形都包含其父六边形的坐标。这样只需要规定好每个网格的子网格坐标的计算方法,对于子网格,只需在父网格的坐标后面追加子网格的坐标即可。这样一来,只需关注一个网格的7个子网格如何计算坐标,所有层级的每个网格的坐标都可以递归得到,这7个网格坐标的计算方法见图7,为了表述方便,我称之为**IJK七网格坐标系**。那么还有12个五边形咋办?不好意思,那12个五边形随着层级划分仍然是12个,层级越高这些五边形越小,并且都在海里,影响不大,直接不管了!

图7 七个网格的坐标
同时,H3还专门设置了一种叫做网格有向边(directed edges of grid cells)的东西(如图8),其目的是为了网格能够快速地找到其某个邻网格。这种网格有向边分为单向网格有向边和双向网格有向边,比如说,对于两个相邻网格A和B,其相交的边是E,如果E是一条由A->B的单向边,那么只能通过A快速找到邻居B,B不能快速找到邻居A;而如果E是一条双向边,那么A可以快速地找到B,B也可以快速地找到A。

图8 网格有向边
那么,说了这么多,终于要说到最关键的H3索引值了。 H3的索引值最多占63个比特位,可以用一个长整型表示,其结构如下所示: 0-3位 :索引模式。0表示无效,1表示普通的网格,2表示单向边,3表示双向边(我也不知道为啥要用4个比特位表示,明明表示4种模式两位就够了,可能是为了以后能够加模式吧); 4-6位 :如果索引模式是0或1,则没用;如果是2或3,表示这条边是这个六边形的哪条边,取值范围1-6; 7-10位 :表示层级,取值范围0-15; 11-17位 :表示这个网格属于哪个基网格,取值范围0-121; 18-21位 :表示这个网格的第一代祖宗网格在IJK七网格坐标系 下的坐标; n-n+2(n<=61)位 :同上,表示这个网格的第i代祖宗网格在IJK七网格坐标系下的坐标

可以看到,其实对于层级数比较小的网格,后面很多位是不需要的,如果全置为0则会浪费很多空间,所以H3索引值是可以压缩的。压缩算法在官方文档里没有讲,需要看代码才能知道了,这都是后话。

总结

Uber提出的这个H3索引其实有很强的实用性,除了上述的基本原理外,H3还支持空间覆盖和各种查询功能。虽然Uber批判了一番传统的空间索引,但是个人感觉H3并不是在所有时空查询/空间查询场景下都适用,例如在空间范围矩形范围查询场景下,其效率可能不如GeoHash/Z2/S2/Hilbert。并且H3舍弃了一部分海上的区域,只适用于陆地上空间数据的索引,对于轮船轨迹之类的空间数据就不太适用了。并且Uber自己也说,H3现在用于市场的定量分析(H3 is used throughout Uber to support quantitative analysis of our marketplace),H3还有没有被Uber用到其他地方,也许只有Uber自己知道。 原理讲解就到这里,H3的使用方法和源码还请移步文章开头给出的H3官方文档和项目地址。后续如果有时间我会再探究一下H3的源码学习一下。

相关推荐
秋夜Autumn18 分钟前
贪心算法相关知识
算法·贪心算法
小懒编程日记31 分钟前
【数据结构与算法】B树
java·数据结构·b树·算法
心怀花木38 分钟前
【算法】双指针
c++·算法
闫铁娃39 分钟前
二分解题的奇技淫巧都有哪些,你还不会吗?
c语言·数据结构·c++·算法·leetcode
Y_3_741 分钟前
【回溯数独】有效的数独(medium)& 解数独(hard)
java·数据结构·windows·算法·dfs·回溯
shan_shmily2 小时前
算法知识点————贪心
算法
寂柒2 小时前
C++——模拟实现stack和queue
开发语言·c++·算法·list
熬夜学编程的小王2 小时前
C++类与对象深度解析(一):从抽象到实践的全面入门指南
c++·git·算法
CV工程师小林2 小时前
【算法】DFS 系列之 穷举/暴搜/深搜/回溯/剪枝(下篇)
数据结构·c++·算法·leetcode·深度优先·剪枝
Dylanioucn2 小时前
【分布式微服务云原生】掌握 Redis Cluster架构解析、动态扩展原理以及哈希槽分片算法
算法·云原生·架构