常用场景
- 图像处理
- 空间数据索引
- 2D 中的快速碰撞检测
- 稀疏数据
算法场景一:游戏开发碰撞检测
游戏开发中经常有碰撞检测的算法,对于平面上N个图形,如果需要检测互相之间是否发生碰撞。
基本算法:进行N(N-1)次比较,时间复杂度是O(n^2)四叉树算法:时间复杂度是O(nlog n)
算法场景二:地图上标注的显示逻辑
为了保证规律性和平均分配,采用区域划分方法显示标注。简单来说就是把屏幕分割成若干个区域,每个区域最多显示一个标注,然后根据地图缩放比例动态的设置这些区域的大小以达到最佳的用户体验。
解决:非满四叉树算法如果有100条数据,我们可以嵌套循环找到合适的点放入相应的区域,循环10000次,如果有10000条数据,可能我们的操作系统就要抓狂了。这种计算方法是低效的,时间复杂度至少为O(n^2)。
实现过程
1)创建四叉树
将数据逐个放入这个树状结构,给每一个节点一个范围。创建过程较为费时,建议另开线程。
2)四叉树查找出需要的标注
首先,我们要理清一个逻辑,如果数据量很大,构建四叉树仍然需要一些时间,但是这个时间是我们允许的,相信你也不会频繁的请求大量标注数据。
关键问题就是在用户拖拽旋转结束的时候,我们需要调整我们界面的显示UI,也就是说,关键问题就是从这个四叉树里面查找出需要的标注,这部分的速度至关重要。
四叉树没有删除的方法,因为删除成本大于重建树结构成本。
主要逻辑:第一步:遍历屏幕划分的区域 第二步:比较该区域是否和四叉树元素范围有交集,无交集则舍弃,有交集继续向下查找
优化:我对查找逻辑做了小优化。在第二步的时候,如果有交集,给是否继续向下查找做了一个开关。理由是,我们查找出所有在该划分区域内的标注,如果目的只是为了算出他们的平均中心点,就显得意义不大,大可查找出几个就停止查找。
聚合分为四个级别:市、区、街道,点(不聚合)
非满四叉树优化:为每个结点添加一个“容量”的属性,在四叉树初始化时只有一个根结点,在插入数据时,如果一个结点内的数据量大于了结点“容量”,再将结点进行分裂。如此,可以保证每个结点内都存储着数据,避免了内存空间的浪费。
查询:只有找到了位置对应的结点,那么结点下的所有点都会是此位置的附近点,更小的“容量”意味着每个结点内点越少,也就意味着查询的精度会越高。
边界点问题:每个结点内的点必然是相邻的,但相邻的点越不一定在同一个结点内
特性: 字典树,又称前缀树或trie树,是一种有序树,用于保存关联数组,其中的键通常是字符串。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。
解决: 继续优化四叉树,给结点添加一个“编号”属性。
代码实现
1)四分区域标识
typedef enum { UR = 0, // UR第一象限 UL = 1, // UL为第二象限 LL = 2, // LL为第三象限 LR = 3 // LR为第四象限 }QuadrantEnum;
2)空间对象数据结构
typedef struct SHPMBRInfo { int nID; // 空间对象ID号 MapRect Box; // 空间对象MBR范围坐标 }SHPMBRInfo;
3)四叉树节点数据结构
typedef struct QuadNode { MapRect Box; // 节点所代表的矩形区域 Int nShpCount; // 节点所包含的所有空间对象个数 SHPMBRInfo* pShapeObj; // 空间对象指针数组 Int nChildCount; // 子节点个数 QuadNode *children[4]; // 指向节点的四个孩子 }QuadNode;
四叉树原理
四叉树是种很直接的空间索引技术。在四叉树中,每个节点表示覆盖了部分进行索引的空间的边界框,根节点覆盖了整个区域。每个节点要么是叶节点,有包含一个或多个索引点的列表,没有孩子。要么是内部节点,有四个孩子,每个孩子对应将区域沿两根轴对半分得到的四个象限中的一个,四叉树也因此得名。
四叉树的操作
1.节点分裂
- 当满足特定条件时,为了获得更好的检索效率,四叉树的节点会发生分裂,分裂的做法是:以当前节点的矩形为中心, 划分出四个等大的矩形,作为四个子节点,每个节点深度=当前节点深度+1,根节点深度为0;并且将当前节点的元素重新插入到对应的子节点。
2.插入元素
- 平面的元素多种多样,点,线,图形,但都能够做一个统一,第一步都是要找出图形所覆盖的节点,这里以矩形为例
- 从根节点开始,如果当前节点无子节点,将元素插入到当前节点。如果存在子节点K,并且元素能够完全被子节K点所“包围”,就将元素插入到子节点K,对于子节点K进行上述递归操作;如果元素跨越了多个子节点,那就将元素存储在当前节点
- 如果某一节点元素超过特定值,并且深度小于四叉树允许的最大深度,分裂当前节点。
3.检索
- 对于给定检索区域,从根节点起,如果检索区域与当前节点有交集,返回当前节点的所有元素。
- 如果当前节点还有子节点,递归检索与检索区域有交集的子节点。
业务定义
后台实现聚合算法,App 拿到数据后,直接显示聚合点或非聚合点。具体实现如下:
- App 传入缩放级别,来获取标注数据,标注数据需要一个字段来标识这些点的聚合级别(市、区、街道,点)。
- 根据标注数据向地图上添加标注。
- 地图缩放时,重复步骤 1 和 步骤 2。这里需要注意的是,我们要将地图缩放级别按照聚合级别划分区间,同一区间内避免多次获取数据(同一聚合级别的数据没有变化,或者说变化不大,可以依赖于其他的数据刷新机制,如:再次进入 App)。
源码
API 设计合理 + 中文注释:github.com/hadesh/iOS_…